]>Динамическое распределение памяти

Динамическое распределение памяти

Аватар пользователя mabu

Время жизни локальных переменных ограничено временем выполнения функции. После завершения функции они автоматически уничтожаются.

Глобальные переменные живут до завершения программы, но имеют заранее известный размер, поэтому они не подходят, когда требуется хранить и обрабатывать данные неизвестного размера и длины.

Динамическое выделение и освобождение памяти работает по запросу. Как следствие, ты можешь:

Во фрибейсике можно использовать как встроенные функции выделения памяти, так и функции операционной системы.

  1. Случаи, где FreeBASIC автоматически управляет памятью
  2. Встроенные функции работы с памятью
    1. Allocate
    2. Deallocate
    3. New
    4. Delete
  3. Кучи памяти
    1. Стандартная куча процесса
    2. Создание дополнительных куч
    3. Выделение памяти из кучи
    4. Освобождение памяти из кучи
    5. Уничтожение куч
  4. Ошибки при использовании динамической памяти
    1. Отсутствие проверки успешного выделения памяти
    2. Утечки памяти
    3. Повторное освобождение
    4. Освобождение памяти не своей функцией
    5. Обращение к неинициализированному указателю
    6. Освобождение нединамической памяти
  5. Пример

Случаи, где FreeBASIC автоматически управляет памятью

Везде, где используются строки переменной длины типа String: объединение, присваивание, создание.

Код FreeBASIC
' Компилятор создаёт объект
Dim a As String

' Компилятор выделяет необходимое количество памяти под строку и копирует туда строковый литерал
a = "Hello"
Print a

' В этом месте также выделяется память для новой строки
a += ", world!"
Print a

' Здесь создаётся строка переменной длины сразу же с захватом памяти и копированием туда строкового литерала
var b = "Welcome to FreeBASIC"

' Здесь уже создаётся временный объект, состоящий из объединения нескольких строк, который сразу же уничтожается после вывода его на печать
Print b + "! " + a

' После выхода из зоны видимости все объекты уничтожаются, память освобождается

Встроенные функции работы с памятью

Allocate

Выделяет блок памяти из свободной.

Код FreeBASIC
Declare Function Allocate( _
&t;ByVal Count As UInteger _
)As Any Ptr

Параметры

Count
Количество требуемых байт в блоке.

Возвращаемое значение

В случае успеха функция возвращает указатель на первый байт блока памяти.

В случае ошибки функция возвращает 0.

Описание

Функция Allocate выделяет блок памяти, количеством Count байт, и возвращает указатель на начало блока. Содержимое выделенного блока не инициализируется нулями или чем‐либо ещё, оно остаётся с неопределёнными значениями (так называемый мусор).

Тип возвращаемых данных — это Any Ptr, поэтому результат может быть приведен к любому желаемому типу данных.

Когда блок памяти больше не нужен, то выделенную таким способом память следует освобождать функцией Deallocate.

Памяти может не хватить, поэтому всегда следует проверять возвращаемое значение на 0, чтобы избежать ошибок.

Deallocate

Освобождает ранее выделенный блок памяти.

Код FreeBASIC
Declare Sub Deallocate( _
&t;ByVal Pointer As Any Ptr _
)

Параметры

Pointer
Указатель на ранее выделенный функцией Allocate блок памяти.

Возвращаемое значение

Функция не возвращает значений.

Описание

Функция Deallocate освобождает место в памяти. Блок памяти, ранее выделенный с помощью вызова Allocate отмечается как свободный. Освобождённая память может захватываться программами или операционной системой. Обрати внимание, что эта функция оставляет значение параметра Pointer неизменным, он по‐прежнему будет указывать на тот же блок памяти, а не на нулевой указатель.

Освобождать один и тот же блок памяти более одного раза нельзя. Это ошибка, приводящая к неопределённому поведению.

После вызова Deallocate блок памяти уже недоступен программе, поэтому использовать указатель на него в дальнейшем нельзя. Некоторые программисты сразу же после вызова Deallocate присваивают 0 этому указателю, чтобы избежать ошибок.

Функции встроенной библиотеки могут не сразу отдавать память операционной системе. Хотя для самой программы эта память помечена как свободная, но операционная система видит её до сих пор зарезервированной за этой программой. Такое поведение упрощает последующие выделения памяти, экономя несколько системных вызовов.

Пример

Код FreeBASIC
' Будем выделять 8 килобайт
Const BufferSize As Integer = 1024 * 8

' Выделить кусок памяти определённого размера
Dim buffer As Byte Ptr = Allocate(BufferSize)

' Проверить на ошибку и завершиться, если не удалось выделить память
If buffer = 0 Then
&t;Print "Не могу выделить память"
&t;End(1)
End If

' Что‐нибудь сделать с памятью
For i As Integer = 0 To BufferSize - 1
&t;buffer[i] = 123
Next

' Память больше не нужна, возвращаем её
Deallocate(buffer)

New

Выделяет память под объект. Особенность этой функции: после того, как компилятор встретит её в тексте, он автоматически добавит вызов конструктора объекта.

Есть два варианта этой функции:

Результатом выполнения функции New будет указатель на отведенную память или 0 в случае ошибки.

Delete

Освобождает память, занятую объектом. Перед этим компилятор автоматически добавляет вызов деструктора объекта.

Освобождение памяти связано с тем, как она выделялась: для одного элемента или для нескольких. В соответствии с этим существуют две формы применения Delete:

Освобождаться с помощью Delete может только память, выделенная функцией New.

Пример с одиночным объектом

Код FreeBASIC
' Объявим какой‐нибудь класс, например, обыкновенную дробь
Type Rational
&t;Dim Numerator As Integer
&t;Dim Denominator As Integer
End Type

' Создаём объект, компилятор также вызовет его конструктор
Dim pRational As Rational Ptr = New Rational(3, 4)

' Удаление объекта
Delete pRational

Пример с массивом объектов

Код FreeBASIC
' Выделяем память для 100 целых чисел
Dim p As Integer Ptr = New Integer[100]

' Присвоим какие‐нибудь значения элементам в этом массиве
For i As Integer = 0 To 99
&t;p[i] = i
Next

' Удаление массива объектов
Delete[] p

Кучи памяти

Куча памяти — это специальный регион виртуальной памяти, с помощью которого в операционной системе реализована динамически распределяемая память процесса. Физически представляет собой область памяти, поделённую на занятые и свободные части различного размера. При запуске любого процесса операционная система создаёт для него одну стандартную кучу. В дальнейшем можно самостоятельно создавать и уничтожать кучи.

Все кучи принадлежат и доступны только тому процессу, в котором были созданы. Вся выделенная память из кучи является «собственными байтами» процесса. Если библиотека динамический компоновки (DLL) создаёт дополнительную кучу, то она также будет принадлежать вызвавшему эту библиотеку процессу.

Для того, чтобы запросить память у операционной системы, необходимо каким‐либо образом получить описатель кучи: либо из стандартной кучи процесса, либо из самостоятельно созданной.

Стандартная куча процесса

Создаётся операционной системой при запуске процесса и автоматически уничтожается при его завершении. Самостоятельно уничтожить такую кучу нельзя.

Получить описатель стандартной кучи процесса можно функцией GetProcessHeap.

Код FreeBASIC
Declare Function GetProcessHeap( _
)As HANDLE

Параметры

Функция не имеет параметров.

Возвращаемое значение

В случае успеха функция возвращает описатель стандартной кучи процесса.

В случае ошибки функция возвращает 0.

Описание

Функция GetProcessHeap получает описатель кучи, который можно использовать для выделения памяти без необходимости создания дополнительных куч.

Все глобальные переменные хранятся в стандартной куче процесса.

Создание дополнительных куч

Во многих случаях одной кучи вполне достаточно, но в силу ряда причин иногда целесообразно иметь в своём распоряжении несколько куч:

Дополнительные кучи можно создать функцией HeapCreate.

Код FreeBASIC
Declare Function HeapCreate( _
&t;ByVal flOptions As DWORD, _
&t;ByVal dwInitialSize As SIZE_T_, _
&t;ByVal dwMaximumSize As SIZE_T_ _
)As HANDLE

Параметры

flOptions
Специальные флаги, модифицирующие выполняемые операции с кучей. Самый интересный — это HEAP_NO_SERIALIZE, который заставит операционную систему отключить синхронизацию выделения памяти. Если не уверен, просто указывай здесь 0.
dwInitialSize
Количество байтов памяти, первоначально выдаваемых куче. Может быть 0.
dwMaximumSize
Указывает максимальный размер, до которого может расширяться куча. Если он больше 0, то куча будет именно такого размера и не сможет увеличиваться. Если этот параметр равен 0, то система резервирует регион и, если надо, расширяет его до максимально возможного объёма.

Возвращаемое значение

Если функция завершилась успешно, то возвращается описатель созданной кучи.

В случае ошибки возвращается 0.

Описание

Функция создаёт кучу, из которой вызывающий процесс может запрашивать память.

Если флаг HEAP_NO_SERIALIZE не указан (значение по умолчанию), то операционная система синхронизирует доступ к куче в вызывающем процессе. Синхронизация обеспечивает взаимное исключение при попытке двух или более потоков одновременно выделить или освободить блоки памяти из одной кучи. Синхронизация требует небольших затрат на производительность, её необходимо использовать всякий раз, когда несколько потоков выделяют и освобождают память из одной кучи. Чтобы не повредить данные в куче, флаг HEAP_NO_SERIALIZE следует указывать только в следующих случаях:

Если ты не уверен, нужен ли тебе флаг HEAP_NO_SERIALIZE, то лучше не пользуйся им.

Выделение памяти из кучи

Для этого достаточно вызвать функцию HeapAlloc.

Код FreeBASIC
Declare Function HeapAlloc( _
&t;ByVal hHeap As HANDLE, _
&t;ByVal dwFlags As DWORD, _
&t;ByVal dwBytes As SIZE_T_ _
)As Any Ptr

Параметры

hHeap
Описатель кучи, из которой запрашивается блок памяти.
dwFlags
Комбинация флагов:
  • HEAP_NO_SERIALIZE — чтобы не синхронизировать доступ к куче из разных потоков;
  • HEAP_ZERO_MEMORY — если требуется обнулить блок памяти, без этого флага память отдаётся без очистки, там может быть всё, что угодно.
dwBytes
Размер требуемого блока в байтах.

Возвращаемое значение

Если функция завершилась успешно, то возвращается указатель на выделенную память.

В случае ошибки возвращается 0.

Описание

Функция HeapAlloc выделяет блок памяти, количеством dwBytes байт, и возвращает указатель на начало блока. Содержимое выделенного блока можно обнулить, если указать флаг HEAP_ZERO_MEMORY, или оставить как есть с неопределёнными значениями (так называемый мусор).

Тип возвращаемых данных — это Any Ptr, поэтому результат может быть приведен к любому желаемому типу данных.

Когда блок памяти больше не нужен, то выделенную таким способом память следует освобождать функцией HeapFree.

Всегда следует проверять возвращаемое значение на 0, чтобы избежать ошибок.

Освобождение памяти из кучи

Если память больше не нужна, её следует отдать операционной системе функцией HeapFree.

Код FreeBASIC
Declare Function HeapFree( _
&t;ByVal hHeap As HANDLE, _
&t;ByVal dwFlags As DWORD, _
&t;ByVal lpMem As Any Ptr _
)As Integer

Параметры

hHeap
Описатель кучи, из которой освобождается память.
dwFlags
Флаги доступа к куче, может быть 0 или HEAP_NO_SERIALIZE.
lpMem
Указатель на память, которую следует вернуть системе.

Возвращаемое значение

Если функция завершилась успешно, то возвращается любое ненулевое значение.

В случае ошибки возвращается 0.

Описание

Функция даёт указание операционной системе отметить участок памяти как свободный. В то же время система необязательно сразу же забирает его себе, она может придержать его за процессом для лучшей производительности в операциях последующего выделения памяти. Так или иначе, переменная, которая ссылалась на этот блок памяти, теперь ссылается на недействительные адреса, использовать её больше нельзя, некоторые программисты присваивают ей 0, чтобы исключить ошибки.

Уничтожение куч

Когда куча больше не нужна, её следует уничтожить функцией HeapDestroy.

Код FreeBASIC
Declare Function HeapDestroy( _
&t;ByVal hHeap As HANDLE _
)As Integer

Параметры

hHeap
Описатель кучи, которую требуется уничтожить.

Возвращаемое значение

Если функция завершилась успешно, то возвращается любое ненулевое значение.

В случае ошибки возвращается 0.

Описание

Это действие уничтожает дополнительну кучу и лишает законной силы описатель кучи, который больше не может использоваться программой. Когда куча разрушается, то освобождаются все выделенные из неё блоки памяти и возвращаются системе.

Это удобно, когда требуется уничтожить какой‐нибудь список или дерево, целиком располагающеися в одной куче: в таком случае вместо последовательных вызовов освобождения памяти для каждого элемента списка, достаточно один раз вызвать HeapDestroy. Все переменные, которые ранее ссылались на блоки памяти в уничтоженной куче, теперь будут указывать на недействительные адреса.

Если не уничтожать кучу самостоятельно, то операционная система уничтожит её автоматически, но только при завершении процесса. Кучи, созданные в других потоках, тоже будут уничтожены только при завершении всего процесса, поэтому хорошей практикой является уничтожение кучи сразу же как только она уже не нужна.

Система не позволит уничтожить стандартную кучу процесса: она уничтожается только при завершении процесса. Если ты передашь указатель стандартной кучи процесса в функцию HeapDestroy, то система проигнорирует такой вызов.

Ошибки при использовании динамической памяти

При работе с динамической памятью можно совершить большое количество ошибок разной степени тяжести. Последствия наступают не сразу при возникновении ошибки, а через некоторое время в процессе выполнения программы. Такие ошибки трудно находить, и потому они особенно опасны.

Отсутствие проверки успешного выделения памяти

Библиотечные функции Allocate или New используют стандартную кучу процесса для выделения памяти. Если затребованный размер памяти слишком большой, то операционная система не будет выделять память и тогда функции вернут нулевое значение. Если это нулевое значение будет присвоено указателю, к которому впоследствии будет применён оператор разыменования указателя или оператор индексирования, то операционная система аварийно завершит работу программы с ошибкой сегментации (Segmentation Fault). Для того чтобы избежать таких ошибок необходимо сразу после выделения памяти проверять возвращаемое значение функций на ноль.

Код FreeBASIC
Const MaxLength As Integer = 1000000000
Dim p As Integer Ptr = Allocate(MaxLength * SizeOf(Integer))
If p = 0 Then
&t;Print "Требуемая память не выделена"
End If

Утечки памяти

Данная ошибка возникает, когда память не освобождается, но перестаёт контролироваться указателем. Подобную ошибку называют утечкой памяти, поскольку такую память невозможно освободить. Ошибка труднонаходима, она не сказывается на работе приложения. Однако при систематических утечках программа требует всё больше памяти у операционной системы. Рано или поздно вступают в действие архитектурные ограничения операционной системы, и тогда новое выделение памяти становится невозможным. В этой ситуации в программе, которая запрашивает память, обычно происходит аварийная остановка.

Утечки памяти происходят по следующим причинам:

Код FreeBASIC
Dim p As Integer Ptr = New Integer
*p = 55
' Выделяется новый кусок памяти в тот же указатель
p = New Integer
' Доступ к старой памяти невозможен, теперь её невозможно освободить, произошла утечка

Повторное освобождение

Попытка передать в функцию указатель на память, которая ранее была освобождена, приведёт к неопределённому поведению. На практике это означает, что может произойти всё, что угодно: программа упадёт, закричит, перестанет отвечать, повредятся важные данные или что‐нибудь ещё.

Код FreeBASIC
' Создаём динамическтй массив из 20 целых чисел
Dim p As Integer Ptr = New Integer[20]
' Освобождаем память
Delete[] p
' Повторно освобождать память нельзя, это приводит к неопрделённому поведению
Delete[] p

Освобождение памяти не своей функцией

Освобождение памяти следует производить соответствующей парной функцией:

Нельзя смешивать эти функции, так как это приводит к неопределённому поведению.

Обращение к неинициализированному указателю

Нельзя обращаться к указателю, которому не присвоен действительный адрес памяти:

Код FreeBASIC
Dim p As Integer Ptr
' Освобождение невыделенной памяти — это ошибка
Delete p

Освобождение нединамической памяти

Нельзя освобождать память, которая не выделена динамически:

Код FreeBASIC
Dim Size As Integer = 42
Dim p As Integer Ptr = @Size
' Овсобождение нединамической памяти — это ошибка
Delete p

Пример

Рассмотрим программу, которая сортирует слова, переданные в командной строке, по алфавиту с помощью двоичного дерева.

Код FreeBASIC
#define unicode
#include once "windows.bi"
#include once "win\shellapi.bi"

' Размер буфера под строку
Const NodeDataBufferSize As Integer = 256

' Узел дерева
Type Node
&t;' Левая ветка
&t;Dim LeftNode As Node Ptr
&t;' Правая ветка
&t;Dim RightNode As Node Ptr
&t;' Данные
&t;Dim Data As WString * (NodeDataBufferSize + 1)
End Type

' Создание узла
Function CreateNode(ByVal hHeap As Handle)As Node Ptr
&t;' Выделить память в куче под объект
&t;Dim pNode As Node Ptr = HeapAlloc(hHeap, HEAP_NO_SERIALIZE, SizeOf(Node))
&t;If pNode <> 0 Then
&t;&t;' Обнулить поля, потому что в них сейчас мусор
&t;&t;pNode->LeftNode = 0
&t;&t;pNode->RightNode = 0
&t;&t;pNode->Data[0] = 0
&t;End If
&t;Return pNode
End Function

' Добавление данных
Sub AddNode(ByVal pNode As Node Ptr, ByVal hHeap As Handle, ByVal s As WString Ptr)
&t;If Len(pNode->Data) = 0 Then
&t;&t;' В узле нет данных, добавить
&t;&t;pNode->Data = *s
&t;Else
&t;&t;Select Case lstrcmp(s, @pNode->Data)
&t;&t;&t;Case Is > 0
&t;&t;&t;&t;' Добавить в правую ветку
&t;&t;&t;&t;If pNode->RightNode = 0 Then
&t;&t;&t;&t;&t;pNode->RightNode = CreateNode(hHeap)
&t;&t;&t;&t;End If
&t;&t;&t;&t;If pNode->RightNode <> 0 Then
&t;&t;&t;&t;&t;AddNode(pNode->RightNode, hHeap, s)
&t;&t;&t;&t;End If
&t;&t;&t;Case 0
&t;&t;&t;&t;' Не добавлять дубликат
&t;&t;&t;Case Is < 0
&t;&t;&t;&t;' Добавить в левую ветку
&t;&t;&t;&t;If pNode->LeftNode = 0 Then
&t;&t;&t;&t;&t;pNode->LeftNode = CreateNode(hHeap)
&t;&t;&t;&t;End If
&t;&t;&t;&t;If pNode->LeftNode <> 0 Then
&t;&t;&t;&t;&t;AddNode(pNode->LeftNode, hHeap, s)
&t;&t;&t;&t;End If
&t;&t;End Select
&t;End If
End Sub

' Печать дерева
Sub PrintTree(ByVal pNode As Node Ptr)
&t;If pNode <> 0 Then
&t;&t;' Печать левой ветки
&t;&t;PrintTree(pNode->LeftNode)
&t;&t;' Печать данных
&t;&t;Print pNode->Data
&t;&t;' Печать правой ветки
&t;&t;PrintTree(pNode->RightNode)
&t;End If
End Sub

' Разбить командную строку на пробелы
Dim ArgsCount As Long = Any
Dim Args As WString Ptr Ptr = CommandLineToArgvW(GetCommandLine(), @ArgsCount)

' Создать кучу
Dim hHeap As Handle = HeapCreate(HEAP_NO_SERIALIZE, 0, 0)
If hHeap <> 0 Then
&t;' Дерево
&t;Dim Tree As Node Ptr = CreateNode(hHeap)
&t;If Tree <> 0 Then
&t;&t;' Добавление данных
&t;&t;For i As Integer = 1 To ArgsCount - 1
&t;&t;&t;AddNode(Tree, hHeap, Args[i])
&t;&t;Next
&t;&t;' Показать результат
&t;&t;PrintTree(Tree)
&t;End If

&t;' Удаление дерева простым уничтожением кучи, в которой оно размещалось
&t;HeapDestroy(hHeap)
End If

LocalFree(Args)

Поделись ссылочкой в социальных сетях