GetMem — Функция Delphi

Содержание

GetMem — Функция Delphi

Для динамического распределения памяти служат еще две тесно взаимосвязанные процедуры: GetMem и FreeMem. Подобно New и Dispose, они во время вызова выделяют и освобождают память для одной динамической переменной:

GetMem(var P: Pointer; Size: Integer) — создает в динамической памяти новую динамическую переменную c заданным размером Size и присваивает ее адрес указателю P. Переменная-указатель P может указывать на данные любого типа.

FreeMem(var P: Pointer [; Size: Integer] ) — освобождает динамическую переменную.

Если в программе используется этот способ распределения памяти, то вызовы GetMem и FreeMem должны соответствовать друг другу. Обращения к GetMem и FreeMem могут полностью соответствовать вызовам New и Dispose.

New(P4); // Выделить блок памяти для указателя P4
.
Dispose(P4); // Освободить блок памяти

Следующий отрывок программы даст тот же самый результат:
GetMem(P4, SizeOf(ShortString)); // Выделить блок памяти для P4
.
FreeMem(P4); // Освободить блок памяти

С помощью процедуры GetMem одной переменной-указателю можно выделить разное количество памяти в зависимости от потребностей. В этом состоит ее основное отличие от процедуры New.

GetMem(P4, 20); // Выделить блок в 20 байт для указателя P4
.
FreeMem(P4); // Освободить блок памяти

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

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

ReallocMem(var P: Pointer; Size: Integer) — освобождает блок памяти по значению указателя P и выделяет для указателя новый блок памяти заданного размера Size. Указатель P может иметь значение nil, а параметр Size — значение 0, что влияет на работу процедуры:

если P = nil и Size = 0, процедура ничего не делает;

если P = nil и Size <> 0, процедура выделяет новый блок памяти заданного размера, что соответствует вызову процедуры GetMem.

если P <> nil и Size = 0, процедура освобождает блок памяти, адресуемый указателем P и устанавливает указатель в значение nil. Это соответствует вызову процедуры FreeMem, с той лишь разницей, что FreeMem не очищает указатель;

если P <> nil и Size <> 0, процедура перевыделяет память для указателя P. Размер нового блока определяется значением Size. Данные из прежнего блока копируются в новый блок. Если новый блок больше прежнего, то приращенный участок остается неинициализированным и содержит случайные данные.

Программирование на языке Delphi
Глава 2. Основы языка Delphi. Часть 4

Файлы

Понятие файла

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

Для файла существует понятие текущей позиции. Она показывает номер элемента, который будет прочитан или записан при очередном обращении к файлу. Чтение-запись каждого элемента продвигает текущую позицию на единицу вперед. Для большинства файлов можно менять текущую позицию чтения-записи, выполняя прямой доступ к его элементам.

В зависимости от типа элементов различают три вида файла:

  • файл из элементов фиксированного размера; элементами такого файла чаще всего являются записи;
  • файл из элементов переменного размера (нетипизированный файл); такой файл рассматривается просто как последовательность байтов;
  • текстовый файл; элементами такого файла являются текстовые строки.

Для работы с файлом в программе объявляется файловая переменная. В файловой переменной запоминается имя файла, режим доступа (например, только чтение), другие атрибуты. В зависимости от вида файла файловая переменная описывается по-разному.

Для работы с файлом, состоящим из типовых элементов переменная объявляется с помощью словосочетания file of, после которого записывается тип элемента:

К моменту такого объявления тип TPerson должен быть уже описан (см. выше).

Объявление переменной для работы с нетипизированным файлом выполняется с помощью отдельного слова file:

Для работы с текстовым файлом переменная описывается с типом TextFile:

Работа с файлами

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

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

В результате этого действия поля файловой переменной F инициализируются начальными значениями. При этом в поле имени файла заносится строка ‘MyFile.txt’.

Так как файла еще нет на диске, его нужно создать:

Теперь запишем в файл несколько строк текста. Это делается с помощью хорошо вам знакомых процедур Write и Writeln:

При работе с файлами первый параметр этих процедур показывает, куда происходит вывод данных.

После работы файл должен быть закрыт:

Рассмотрим теперь, как прочитать содержимое текстового файла. После инициализации файловой переменной (AssignFile) файл открывается с помощью процедуры Reset:

Для чтения элементов используются процедуры Read и Readln, в которых первый параметр показывает, откуда происходит ввод данных. После работы файл закрывается. В качестве примера приведем программу, распечатывающую в своем окне содержимое текстового файла ‘MyFile.txt’:

Так как обычно размер файла заранее не известен, перед каждой операцией чтения вызывается функция Eof, которая возвращает True, если достигнут конец файла.

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

Стандартные подпрограммы управления файлами

Для обработки файлов в языке Delphi имеется специальный набор процедур и функций:

  • AssignFile(var F; FileName: string) — связывает файловую переменную F и файл, имя которого указано в FileName.
  • Reset(var F [: File; RecSize: Word ] ) — открывает существующий файл. Если открывается нетипизированный файл, то RecSize задает размер элемента файла.
  • Rewrite(var F [: File; RecSize: Word ] ) — создает и открывает новый файл.
  • Append(var F: TextFile) — открывает текстовый файл для добавления текста.
  • Read(F, V1 [, V2, . Vn ]) — начиная с текущей позиции, читает из типизированного файла подряд расположенные элементы в переменные V1, V2, . Vn.
  • Read(var F: TextFile; V1 [, V2, . Vn ] ) — начиная с текущей позиции, читает из текстового файла символы или строки в переменные V1, V2, . Vn.
  • Write(F, V1 [, V2, . Vn ]) — начиная с текущей позиции, записывает в типизированный файл значения V1, V2, . Vn.
  • Write(var F: TextFile; V1 [, V2, . Vn ] ) — начиная с текущей позиции указателя чтения-записи, записывает в текстовый файл значения V1, V2, . Vn.
  • CloseFile(var F) — закрывает ранее открытый файл.
  • Rename(var F; NewName: string) — переименовывает неоткрытый файл F любого типа. Новое имя задается в NewName.
  • Erase(var F) — удаляет неоткрытый внешний файл любого типа, заданный переменной F.
  • Seek(var F; NumRec: Longint) — устанавливает позицию чтения-записи на элемент с номером NumRec; F — типизированный или нетипизированный файл.
  • SetTextBuf(var F: TextFile; var Buf [; Size: Word]) — назначает текстовому файлу F новый буфер ввода-вывода Buf объема Size.
  • SetLineBreakStyle(var T: Text; Style: TTextLineBreakStyle) — устанавливает способ переноса строк в файле (одиночный символ #10 или пара символов #13#10).
  • Flush(var F: TextFile) — записывает во внешний файл все символы, переданные в буфер для записи.
  • Truncate(var F) — урезает файл, уничтожая все его элементы, начиная с текущей позиции.
  • IOResult: Integer — возвращает код, характеризующий результат (была ошибка или нет) последней операции ввода-вывода.
  • FilePos(var F): Longint — возвращает для файла F текущую файловую позицию (номер элемента, на которую она установлена, считая от нуля). Не используется с текстовыми файлами.
  • FileSize(var F): Longint — возвращает число компонент в файле F. Не используется с текстовыми файлами.
  • Eoln(var F: Text): Boolean — возвращает булевское значение True, если текущая позиция чтения-записи находится на маркере конца строки. Если параметр F не указан, функция применяется к стандартному устройству ввода с именем Input.
  • Eof(var F): Boolean — возвращает булевское значение True, если текущая позиция чтения-записи находится сразу за последним элементом, и False в противном случае.
  • SeekEoln(var F: Text): Boolean — возвращает True при достижении маркера конца строки. Все пробелы и знаки табуляции, предшествующие маркеру, пропускаются.
  • SeekEof(var F: Text): Boolean — возвращает значение True при достижении маркера конца файла. Все пробелы и знаки табуляции, предшествующие маркеру, пропускаются.

Для работы с нетипизированными файлами используются процедуры BlockRead и BlockWrite. Единица обмена

  • BlockRead(var F: File; var Buf; Count: Word [; Result: Word] ) — считывает из файла F определенное число блоков в память, начиная с первого байта переменной Buf. Параметр Buf представляет любую переменную, используемую для накопления информации из файла F. Параметр Count задает число считываемых блоков. Параметр Result является необязательным и содержит после вызова процедуры число действительно считанных записей. Использование параметра Result подсказывает, что число считанных блоков может быть меньше, чем задано параметром Count.
  • BlockWrite(var F: File; var Buf; Count: Word [; Result: Word]) — предназначена для быстрой передачи в файл F определенного числа блоков из переменной Buf. Все параметры процедуры BlockWrite аналогичны параметрам процедуры BlockRead.
  • ChDir(const S: string) — устанавливает текущий каталог.
  • CreateDir(const Dir: string): Boolean — создает новый каталог на диске.
  • MkDir(const S: string) — аналог функции CreateDir. Отличие в том, что в случае ошибки при создании каталога функция MkDir создает исключительную ситуацию.
  • DeleteFile(const FileName: string): Boolean — удаляет файл с диска.
  • DirectoryExists(const Directory: string): Boolean — проверяет, существует ли заданный каталог на диске.
  • FileAge(const FileName: string): Integer — возвращает дату и время файла в числовом системно-зависимом формате.
  • FileExists(const FileName: string): Boolean — проверяет, существует ли на диске файл с заданным именем.
  • FileIsReadOnly(const FileName: string): Boolean — проверяет, что заданный файл можно только читать.
  • FileSearch(const Name, DirList: string): string — осуществляет поиск заданого файла в указанных каталогах. Список каталогов задается параметром DirList; каталоги разделяются точкой с запятой для операционной системы Windows и запятой для операционной системы Linux. Функция возвращает полный путь к файлу.
  • FileSetReadOnly(const FileName: string; ReadOnly: Boolean): Boolean — делает файл доступным только для чтения.
  • FindFirst/FindNext/FindClose
  • ForceDirectories(Dir: string): Boolean — создает новый каталог на диске. Позволяет одним вызовом создать все каталоги пути, заданного параметром Dir.
  • GetCurrentDir: string — возвращает текущий каталог.
  • SetCurrentDir(const Dir: string): Boolean — устанавливает текущий каталог. Если это сделать невозможно, функция возвращет значение False.
  • RemoveDir(const Dir: string): Boolean — удаляет каталог с диска; каталог должен быть пустым. Если удалить каталог невозможно, функция возвращет значение False.
  • RenameFile(const OldName, NewName: string): Boolean — изменяет имя файла. Если это сделать невозможно, функция возвращет значение False.
  • ChangeFileExt(const FileName, Extension: string): string — возвращает имя файла с измененным расширением.
  • ExcludeTrailingPathDelimiter(const S: string): string — отбрасывает символ-разделитель каталогов (символ ‘/’ — для Linux и ‘\’ — для Windows), если он присутствует в конце строки.
  • IncludeTrailingPathDelimiter(const S: string): string — добавляет символ-разделитель каталогов (символ ‘/’ — для Linux и ‘\’ — для Windows), если он отсутствует в конце строки.
  • ExpandFileName(const FileName: string): string — возвращает полное имя файла (с абсолютным путем) по неполному имени.
  • ExpandUNCFileName(const FileName: string): string — возвращает полное сетевое имя файла (с абсолютным сетевым путем) по неполному имени. Для операционной системы Linux эта функция эквивалентна функции ExpandFileName.
  • ExpandFileNameCase(const FileName: string; out MatchFound: TFilenameCaseMatch): string — возвращает полное имя файла (с абсолютным путем) по неполному имени, допуская несовпадения заглавных и строчных букв в имени файла для тех файловых систем, которые этого не допускают (например, файловая система ОС Linux).
  • ExtractFileDir(const FileName: string): string — выделяет путь из полного имени файла; путь не содержит в конце символ-разделитель каталогов.
  • ExtractFilePath(const FileName: string): string — выделяет путь из полного имени файла; путь содержит в конце символ-разделитель каталогов.
  • ExtractRelativePath(const BaseName, DestName: string): string — возвращает относительный путь к файлу DestName, отсчитанный от каталога BaseName. Путь BaseName должен заканчиваться символом-разделителем каталогов.
  • ExtractFileDrive(const FileName: string): string — выделяет имя диска (или сетевого каталога) из имени файла. Для операционной системы Linux функция возвращает пустую строку.
  • ExtractFileExt(const FileName: string): string — выделяет расширение файла из его имени.
  • ExtractFileName(const FileName: string): string — выделяет имя файла, отбрасывая путь к нему.
  • IsPathDelimiter(const S: string; Index: Integer): Boolean — проверяет, является ли символ S[Index] разделителем каталогов.
  • MatchesMask(const Filename, Mask: string): Boolean — проверяет, удовлетворяет ли имя файла заданной маске.

Указатели

Понятие указателя

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

Не секрет, что любая переменная в памяти компьютера имеет адрес. Переменные, которые содержат адреса других переменных, принято называть указателями. Указатели объявляются точно так же, как и обычные переменные:

Переменная P занимает 4 байта и может содержать адрес любого участка памяти, указывая на байты со значениями любых типов данных: Integer, Real, string, record, array и других. Чтобы инициализировать переменную P, присвоим ей адрес переменной N. Это можно сделать двумя эквивалентными способами:

В дальнейшем мы будем использовать более краткий и удобный второй способ.

Если некоторая переменная P содержит адрес другой переменной N, то говорят, что P указывает на N. Графически это обозначается стрелкой, проведенной из P в N (рисунок 12 выполнен в предположении, что N имеет значение 10):

Рисунок 12. Графическое изображение указателя P на переменную N

Теперь мы можем изменить значение переменной N, не прибегая к идентификатору N. Для этого слева от оператора присваивания запишем не N, а P вместе с символом ^:

Символ ^, записанный после имени указателя, называется оператором доступа по адресу. В данном примере переменной, расположенной по адресу, хранящемуся в P, присваивается значение 10. Так как в переменную P мы предварительно занесли адрес N, данное присваивание приводит к такому же результату, что и

Однако в примере с указателем мы умышленно допустили одну ошибку. Дело в том, что переменная типа Pointer может содержать адреса переменных любого типа, не только Integer. Из-за сильной типизации языка Delphi перед присваиванием мы должны были бы преобразовать выражение P^ к типу Integer:

Согласитесь, такая запись не совсем удобна. Для того, чтобы сохранить простоту и избежать постоянных преобразований к типу, указатель P следует объявить так:

При такой записи переменная P по прежнему является указателем, но теперь ей можно присваивать адреса только целых переменных. В данном случае указатель P называют типизированным, в отличие от переменных типа Pointer, которые называют нетипизированными указателями. При использовании типизированных указателей лучше предварительно вводить соответствующий указательный тип данных, а переменные-указатели просто объявлять с этим типом. Поэтому предыдущий пример можно модифицировать следующим образом:

PInteger — это указательный тип данных. Чтобы отличать указательные типы данных от других типов, будем назначать им идентификаторы, начинающиеся с буквы P (от слова Pointer). Объявление указательного типа данных является единственным способом введения указателей на составные переменные, такие как массивы, записи, множества и другие. Например, объявление типа данных для создания указателя на некоторую запись TPerson может выглядеть так:

Переменная P, описанная с типом данных PPerson, является указателем и может содержать адрес любой переменной типа TPerson. Впредь все указатели мы будем вводить через соответствующие указательные типы данных. Типом Pointer будем пользоваться лишь тогда, когда это действительно необходимо или оправдано.

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

После объявления в секции var указатель содержит неопределенное значение. Поэтому переменные-указатели, как и обычные переменные, перед использованием нужно инициализировать. Отсутствие инициализации указателей является наиболее распространенной ошибкой среди новичков. Причем если использование обычных неинициализированных переменных приводит просто к неправильным результатам, то использование неинициализированных указателей обычно приводит к ошибке «Access violation» (доступ к неверному адресу памяти) и принудительному завершению приложения.

Один из способов инициализации указателя состоит в присваивании ему адреса некоторой переменной соответствующего типа. Этот способ мы уже рассмотрели. Второй способ состоит в динамическом выделении участка памяти под переменную соответствующего типа и присваивании указателю его адреса. Работа с динамическими переменными и есть основное назначение указателей. Размещение динамических переменных производится в специальной области памяти, которая называется Heap (куча). Ее размер равен размеру свободной памяти компьютера.

Для размещения динамической переменной вызывается стандартная процедура

Она выделяет требуемый по размеру участок памяти и заносит его адрес в переменную-указатель P. В следующем примере создаются 4 динамических переменных, адреса которых присваиваются переменным-указателям P1, P2, P3 и P4:

Далее по адресам в указателях P1, P2, P3 и P4 можно записать значения:

В таком контексте динамические переменные P1^, P2^, P3^ и P4^ ничем не отличаются от обычных переменных соответствующих типов. Операции над динамическими переменными аналогичны подобным операциям над обычными переменными. Например, следующие операторы могут быть успешно откомпилированы и выполнены:

После работы с динамическими переменными необходимо освободить занимаемую ими память. Для этого предназначена процедура:

Например, в приведенной выше программе явно не хватает следующих строк:

После выполнения данных утверждений указатели P1, P2, P3 и P4 опять перестанут быть связаны с конкретными адресами памяти. В них будут случайные значения, как и до обращения к процедуре New. Не стоит делать попытки присвоить значения переменным P1^, P2^, P3^ и P4^, ибо в противном случае это может привести к нарушению нормальной работы программы.

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

Поэтому следует четко придерживаться последовательности действий при работе с динамическими переменными:

  • создать динамическую переменную;
  • выполнить с ней необходимые действия;
  • разрушить динамическую переменную.

Операции над указателями

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

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

Использование одинаковых значений в разных указателях открывает некоторые интересные возможности. Так после оператора P3 := P1 изменение значения переменной P3^ будет равносильно изменению значения P1^.

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

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

Чаще всего операции сравнения указателей используются для проверки того, связан ли указатель с динамической переменной. Если еще нет, то ему следует присвоить значение nil (зарезервированное слово):

Установка P1 в nil однозначно говорит о том, что указателю не выделена динамическая память. Если всем объявленным указателям присвоить значение nil, то внутри программы можно легко выполнить тестирование наподобие этого:

Процедуры GetMem и FreeMem

Для динамического распределения памяти служат еще две тесно взаимосвязанные процедуры: GetMem и FreeMem. Подобно New и Dispose, они во время вызова выделяют и освобождают память для одной динамической переменной:

  • GetMem(var P: Pointer; Size: Integer) — создает в динамической памяти новую динамическую переменную c заданным размером Size и присваивает ее адрес указателю P. Переменная-указатель P может указывать на данные любого типа.
  • FreeMem(var P: Pointer [; Size: Integer] ) — освобождает динамическую переменную. Если в программе используется этот способ распределения памяти, то вызовы GetMem и FreeMem должны соответствовать друг другу. Обращения к GetMem и FreeMem могут полностью соответствовать вызовам New и Dispose.

Следующий отрывок программы даст тот же самый результат:

С помощью процедуры GetMem одной переменной-указателю можно выделить разное количество памяти в зависимости от потребностей. В этом состоит ее основное отличие от процедуры New.

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

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

ReallocMem(var P: Pointer; Size: Integer) — освобождает блок памяти по значению указателя P и выделяет для указателя новый блок памяти заданного размера Size. Указатель P может иметь значение nil, а параметр Size — значение 0, что влияет на работу процедуры:

  • если P = nil и Size = 0, процедура ничего не делает;
  • если P = nil и Size <> 0, процедура выделяет новый блок памяти заданного размера, что соответствует вызову процедуры GetMem.
  • если P <>nil и Size = 0, процедура освобождает блок памяти, адресуемый указателем P и устанавливает указатель в значение nil. Это соответствует вызову процедуры FreeMem, с той лишь разницей, что FreeMem не очищает указатель;
  • если P <>nil и Size <> 0, процедура перевыделяет память для указателя P. Размер нового блока определяется значением Size. Данные из прежнего блока копируются в новый блок. Если новый блок больше прежнего, то приращенный участок остается неинициализированным и содержит случайные данные.

Представление строк в памяти

В некоторых случаях динамическая память неявно используется программой, например для хранения строк. Длина строки может варьироваться от нескольких символов до миллионов и даже миллиардов (теоретический предел равен 2 ГБ). Тем не менее, работа со строками в программе осуществляется так же просто, как работа с переменными простых типов данных. Это возможно потому, что компилятор автоматически генерирует код для выделения и освобождения динамической памяти, в которой хранятся символы строки. Но что стоит за такой простотой? Не идет ли она в ущерб эффективности? С полной уверенностью можем ответить, что эффективность программы не только не снижается, но даже повышается.

Физически переменная строкового типа представляет собой указатель на область динамической памяти, в которой размещаются символы. Например, переменная S на самом деле представляет собой указатель и занимает всего четыре байта памяти (SizeOf(S) = 4):

При объявлении этот указатель автоматически инициализируется значением nil. Оно показывет, что строка является пустой. Функция SetLength, устанавливающая размер строки, на самом деле резервирует необходимый по размеру блок динамической памяти и записывает его адрес в строковую переменную:

За оператором присваивания строковых переменных на самом деле кроется копирование значения указателя, а не копирование блока памяти, в котором хранятся символы.

Такой подход весьма эффективен как с точки зрения производительности, так и с точки зрения экономного использования оперативной памяти. Его главная проблема состоит в том, чтобы обеспечить удаление блока памяти, содержащего символы строки, когда все адресующие его строковые переменные прекращают свое существование. Эта проблема эффективно решается с помощью механизма подсчета количества ссылок (reference counting). Для понимания его работы рассмотрим формат хранения строк в памяти подробнее.

Пусть в программе объявлены две строковые переменные:

И пусть в программе существует оператор, присваивающий переменной S1 значение некоторой функции:

Для хранения символов строки S1 по окончании ввода будет выделен блок динамической памяти. Формат этого блока после ввода значения ‘Hello’ показан на рисунке 13:

Рисунок 13. Представление строковых переменных в памяти

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

Если в программе встречается оператор присваивания значения одной строковой переменной другой строковой переменной,

то, как мы уже сказали, копия строки в памяти не создается. Копируется только адрес, хранящийся в строковой переменной, и на единицу увеличивается количество ссылок на строку (рисунок 14).

Рисунок 14. Результат копирования строковой переменной S1 в строковую переменную S2

При присваивании переменной S1 нового значения (например, пустой строки):

количество ссылок на предыдущее значение уменьшается на единицу (рисунок 15).

Рисунок 15. Результат присваивания строковой переменной S1 нового значения (пустой строки)

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

Интересно, а что происходит при изменении символов строки, с которой связано несколько строковых переменных? Правила семантики языка требуют, чтобы две строковые переменные были логически независимы, и изменение одной из них не влияло на другую. Это достигается с помощью механизма копирования при записи (copy-on-write).

Например, в результате выполнения операторов

получим следующую картину в памяти (рисунок 16):

Рисунок 16. Результат изменения символа в строке S1

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

Все, что было сказано выше о представлении в памяти строк, относится только к строкам формата AnsiString. Строки формата WideString тоже хранятся в динамической памяти, но для них не поддерживаются механизм подсчета количества ссылок и механизм копирования по записи. Операция присваивания строковых переменных формата WideString означает выделение нового блока динамической памяти и полное копирование в него всех символов исходной строки. Что же касается коротких строк, то они целиком хранятся по месту объявления: или в области данных программы (если это глобальные переменные), или на стеке (если это локальные переменные). Динамическая память вообще не используется для хранения коротких строк.

Динамические массивы

Одним из мощнейших средств языка Delphi являются динамические массивы. Их основное отличие от обычных массивов заключается в том, что они хранятся в динамической памяти. Этим и обусловлено их название. Чтобы понять, зачем они нужны, рассмотрим пример:

Задать размер массива A в зависимости от введенного пользователем значения невозможно, поскольку в качестве границ массива необходимо указать константные значения. А введенное пользователем значение никак не может претендовать на роль константы. Иными словами, следующее объявление будет ошибочным:

На этапе написания программы невозможно предугадать, какие именно объемы данных захочет обрабатывать пользователь. Тем не менее, Вам придется ответить на два важных вопроса:

  • На какое количество элементов объявить массив?
  • Что делать, если пользователю все-таки понадобится большее количество элементов?

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

Такое решение проблемы является неоптимальным. Если пользователю необходимо всего 10 элементов, программа работает без проблем, но всегда использует объем памяти, необходимый для хранения 100 элементов. Память, отведенная под остальные 90 элементов, не будет использоваться ни Вашей программой, ни другими программами (по принципу «сам не гам и другому не дам»). А теперь представьте, что все программы поступают таким же образом. Эффективность использования оперативной памяти резко снижается.

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

Динамический массив объявляется без указания границ:

Переменная DynArray представляет собой ссылку на размещаемые в динамической памяти элементы массива. Изначально память под массив не резервируется, количество элементов в массиве равно нулю, а значение переменной DynArray равно nil.

Работа с динамическими массивами напоминает работу с длинными строками. В частности, создание динамического массива (выделение памяти для его элементов) осуществляется той же процедурой, которой устанавливается длина строк — SetLength.

Изменение размера динамического массива производится этой же процедурой:

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

При уменьшении размера динамического массива лишние элементы теряютяся.

При увеличении размера динамического массива добавленные элементы не инициализируются никаким значением и в общем случае их значения случайны. Однако если динамический массив состоит из элементов, тип которых предполагает автоматическую инициализацию пустым значением (string, Variant, динамический массив, др.), то добавленная память инициализируется нулями.

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

Элементы динамического массива всегда индексируются от нуля. Доступ к ним ничем не отличается от доступа к элементам обычных статических массивов:

К динамическим массивам, как и к обычным массивам, применимы функции Low и High, возвращающие минимальный и максимальный индексы массива соответственно. Для динамических массивов функция Low всегда возвращает 0.

Освобождение памяти, выделенной для элементов динамического массива, осуществляется установкой длины в значение 0 или присваиванием переменной-массиву значения nil (оба варианта эквивалентны):

Однако Вам вовсе необязательно по окончании использования динамического массива освобождать выделенную память, поскольку она освобождается автоматически при выходе из области действия переменной-массива (удобно, не правда ли!). Данная возможность обеспечивается уже известным Вам механизмом подсчета количества ссылок.

Также, как и при работе со строками, при присваивании одного динамического массива другому, копия уже существующего массива не создается.

В приведенном примере, в переменную B заносится адрес динамической области памяти, в которой хранятся элементы массива A (другими словами, ссылочной переменной B присваивается значение ссылочной переменной A).

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

Для работы с динамическими массивами вы можете использовать знакомую по строкам функцию Copy. Она возвращает часть массива в виде нового динамического массива.

Не смотря на сильное сходство динамических массивов со строками, у них имеется одно существенное отличие: отсутствие механизма копирования при записи (copy-on-write).

Нуль-терминированные строки

Кроме стандартных строк ShortString и AnsiString, в языке Delphi поддерживаются нуль-терминированные строки языка C, используемые процедурами и функциями Windows. Нуль-терминированная строка представляет собой индексированный от нуля массив ASCII-символов, заканчивающийся нулевым символом #0. Для поддержки нуль-терминированных строк в языке Delphi введены три указательных типа данных:

Типы PAnsiChar и PWideChar являются фундаментальными и на самом деле используются редко. PChar — это обобщенный тип данных, в основном именно он используется для описания нуль-терминированных строк.

Ниже приведены примеры объявления нуль-терминированных строк в виде типизированных констант и переменных:

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

переменная S3 получит адрес уже существующей строки ‘Object Pascal’.

Для удобной работы с нуль-терминированными строками в языке Delphi предусмотрена директива $EXTENDEDSYNTAX. Если она включена (ON), то появляются следующие дополнительные возможности:

  • массив символов, в котором нижний индекс равен 0, совместим с типом PChar;
  • строковые константы совместимы с типом PChar.
  • указатели типа PChar могут участвовать в операциях сложения и вычитания с целыми числами; допустимо также вычитание (но не сложение!) указателей.

В режиме расширенного синтаксиса допустимы, например, следующие операторы:

В языке Delphi существует богатый набор процедур и функций для работы с нуль-терминированными строками (см. справочник по среде Delphi).

Переменные с непостоянным типом значений

Тип данных Variant

В среде Delphi определен стандартный тип данных Variant, с помощью которого объявляются переменные с непостоянным типом значений. Такие переменные могут принимать значения разных типов данных в зависимости от типа выражения, в котором используются. Следующий пример хорошо демонстрирует мощь переменных с непостоянным типом значений:

Значения переменных с типом Variant

Переменные с непостоянным типом содержат целые, вещественные, строковые, булевские значения, дату и время, массивы и др. Кроме того, переменные с типом Variant принимают два специальных значения: Unassigned и Null.

Значение Unassigned показывает, что переменная является нетронутой, т.е. переменной еще не присвоено значение. Оно автоматически устанавливается в качестве начального значения любой переменной с типом Variant.

Значение Null показывает, что переменная имеет неопределенное значение. Если в выражении участвует переменная со значением Null, то результат всего выражения тоже равен Null.

Переменная с типом Variant занимает в памяти 16 байт. В них хранятся текущее значение переменной (или адрес значения в динамической памяти) и тип этого значения.

Тип значения выясняется с помощью функции

Возвращаемый результат формируется из констант, перечисленных в таблице 2.10. Например, следующий условный оператор проверяет, содержит ли переменная строку (массив строк):

Код типа

Значение

Описание varEmpty $0000 Переменная содержит значение Unassigned. varNull $0001 Переменная содержит значение Null. varSmallint $0002 Переменная содержит значение типа Smallint. varInteger $0003 Переменная содержит значение типа Integer. varSingle $0004 Переменная содержит значение типа Single. varDouble $0005 Переменная содержит значение типа Double. varCurrency $0006 Переменная содержит значение типа Currency. varDate $0007 Переменная содержит значение типа TDateTime. varOleStr $0008 Переменная содержит ссылку на строку формата Unicode в динамической памяти. varDispatch $0009 Переменная содержит ссылку на интерфейс IDispatch (интерфейсы рассмотрены в главе 6). varError $000A Переменная содержит системный код ошибки. varBoolean $000B Переменная содержит значение типа WordBool. varVariant $000C Элемент варьируемого массива содержит значение типа Variant (код varVariant используется только в сочетании с флагом varArray). varUnknown $000D Переменная содержит ссылку на интерфейс IUnknown (интерфейсы рассмотрены в главе 6). varShortint $0010 Переменная содержит значение типа Shortint varByte $0011 Переменная содержит значение типа Byte. varWord $0012 Переменная содержит значение типа Word varLongword $0013 Переменная содрежит значение типа Longword varInt64 $0014 Переменная содержит значение типа Int64 varStrArg $0048 Переменная содержит строку, совместимую со стандартом COM, принятым в операционной системе Windows. varString $0100 Переменная содержит ссылку на длинную строку. varAny $0101 Переменная содержит значение любого типа данных технологии CORBA Флаги varTypeMask $0FFF Маска для выяснения типа значения. varArray $2000 Переменная содержит массив значений. varByRef $4000 Переменная содержит ссылку на значение.

Таблица 10. Коды и флаги варьируемых переменных

позволяет вам преобразовать значение варьируемой переменной к нужному типу, например:

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

Delphi + ассемблер

В процессе разработки программы вы можете неожиданно обнаружить, что описанных выше средств языка Delphi для решения некоторых насущных проблем явно недостаточно. Например, организация критичных по времени вычислений требует использования ассемблера. Кроме того, часто возникает необходимость включить в программу на языке Delphi откомпилированные ранее процедуры и функции, написанные на ассемблере. Разработчики языка учли эти проблемы и дали программисту необходимые средства их решения.

Встроенный ассемблер

Пользователю предоставляется возможность делать вставки на встроенном ассемблере в исходный текст на языке Delphi.

К встроенному ассемблеру можно обратиться с помощью зарезервированного слова asm, за которым следуют команды ассемблера и слово end:

На одной строке можно поместить несколько операторов ассемблера, разделенных двоеточием. Если каждый оператор размещен на отдельной строке, двоеточие не ставится.

В языке Delphi имеется возможность не только делать ассемблерные вставки, но писать процедуры и функции полностью на ассемблере. В этом случае тело подпрограммы ограничивается словами asm и end (а не begin и end), между которыми помещаются инструкции ассемблера. Перед словом asm могут располагаться объявления локальных констант, типов, и переменных. Например, вот как могут быть реализованы функции вычисления минимального и максимального значения из двух целых чисел:

Обращение к этим функциям имеет привычный вид:

Подключение внешних подпрограмм

Программисту предоставляется возможность подключать к программе или модулю отдельно скомпилированные процедуры и функции, написанные на языке ассемблера или C. Для этого используется директива компилятора $LINK и зарезервированное слово external. Директива указывает подключаемый объектный модуль, а external сообщает компилятору, что подпрограмма внешняя.

Предположим, что на ассемблере написаны и скомпилированы функции Min и Max, их объектный код находится в файле MINMAX.OBJ. Подключение функций Min и Max к программе на языке Delphi будет выглядеть так:

В модулях внешние подпрограммы подключаются в разделе implementation.

Итоги

Все, что вы изучили, называется языком Delphi. Мы надеемся, что вам понравились стройность и выразительная сила языка. Но это всего лишь основа. Теперь пора подняться на следующую ступень и изучить технику объектно-ориентированного программирования, без которого немыслимо стать профессиональным программистом. Именно этим вопросом в рамках применения объектов в среде Delphi мы и займемся в следующей главе.

Дополнительная информация

За дополнительной информацией обращайтесь в компанию Interface Ltd.

Использование кучи в Delphi.

Программируя в Delphi мы постоянно явно или неявно взаимодействуем с менеджером кучи. Неявно его используют все функции или конструкции языка, требующие выделения памяти: создания объекта класса, создание динамического массива или длинной строки. Явное взаимодействие с этим механизмом происходит при использовании следующих функций Delphi: New, Dispose, GetMem, AllocMem, ReallocMem, FreeMem.

procedure New(var P: Pointer);

Функция выделяет динамическую память в куче. Параметром принимает типизированный указатель, затем инициирует его адресом выделенного участка. Не имеет смысла использовать функцию с нетипизированным указателем, он просто останется со значением NIL.

procedure Dispose(var P: Pointer);

Функция освобождает память в куче. Параметром принимает типизированный указатель возвращенный функцией New(). Используется только в паре с функцией выделения памяти New().

function summ( var1, var2: integer ): integer;var f_int : ^integer; s_int : ^integer; begin // выделим память new(f_int); new(s_int); f_int^ := var1; s_int^ := var2; Result := f_int^ + s_int^; // освободим память dispose(f_int); dispose(s_int); end;

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

procedure GetMem(var P: Pointer; Size: Integer);

Выделяет блок памяти указанного размера. Можно использовать нетипизированный указатель. Если свободной памяти не окажется, то будет сгенирированно исключение EOutOfMemory. После выделения в памяти содержится всякий мусор.

function AllocMem(Size: Cardinal): Pointer;

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

procedure ReallocMem(var P: Pointer; Size: Integer);

Деиствие зависит от значений P и Size. P может быть пустым указателем или содержать адрес участка памяти, возвращенный функциями GetMem, AllocMem и ReallocMem.
Варианты:
( ( P = NIL) and ( Size = 0 ) ): ReallocMem ничего ни делает;

( ( P = NIL ) and ( Size <> 0 ) ): выделяет новый блок памяти и устанавливает P на его адрес. Можно использовать вместо GetMem();

( ( P <> NIL ) and ( Size = 0 ) ): освобождает память, адресуемую P. P будет установлен в NIL. Похоже на FreeMem(), но в отличае от него чистит указатель.

( ( P <> 0 ) and ( Size <> 0 ) ): перевыделяет указанный блок памяти (изменяет его размер). Существующие данные затронуты не будут, но если память увеличиться, то новое пространство будет содержать всякий мусор. Если для изменения размера не будет хватать памяти, то блок может быть перенесен на другое место в пределах кучи, P будет указывать на новое место.

procedure FreeMem(var P: Pointer[; Size: Integer]);

Функция освобождает память, выделенную в GetMem и AllocMem. Может принимать размер памяти, которую нужно освободить. Надо быть крайне осторожным с этим параметром, так как тут может появиться утечка. После освобождения памяти указатель P будет содержать мусор. Если в качестве параметра передана структура, содержащая длинные строки, варианты, динамические массивы или интерфейсы, тогда перед выполнением FreeMem будет вызвана Finalize.

procedure useMemoryManager;type // шаблончик для доступа к памяти побайтово memcells = array[0..$7FFFFFFE] of byte; var p: pointer; i: integer; begin // выделим память GetMem(p, 100); // и инициируем ее какими-нибудь числами for i:=0 to 99 do memcells(p^)[i] := byte(i); // добавим памяти ReallocMem(p, 200); // и допишим числа for i:=0 to 99 do memcells(p^)[i+100] := byte(i+100); // … посмотрим, что получилось for i:=0 to 199 do writeln(inttostr(memcells(p^)[i])); // уберем после себя FreeMem(p);end;

Работа с менеджером памяти

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

Во-первых есть несколько глобальных переменных, управляемых менеджером.

var AllocMemCount: integer;

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

var AllocMemSize: integer;

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

var HeapAllocFlags: word;

Набор флагов, устанавливающих опции для менеджера памяти. (по умолчанию – GMEM_MOVEABLE)

GMEM_FIXED Выделяет фиксированную память. Т.к. ОС не может перемещать блоки памяти в юзермод, то и нет нужды блокировать память (не может комбинироваться с GMEM_MOVEABLE)
GMEM_MOVEABLE Выделяет перемещаемую память. В юзермод блоки не могут быть перемещены, если они расположены в физической памяти, но могут перемещаться в пределах кучи.
GMEM_ZEROINIT При выделении памяти (например, функцией GetMem) все байты этой памяти будут выставлены в 0
GMEM_MODIFY Используется для изменения атрибутов выделенного блока памяти
GMEM_DDESHARE Введёны для совместимости с 16-разрядными версиями, но может использоваться для оптимизации DDE операций.
GMEM_SHARE — // —
GPTR GMEM_FIXED + GMEM_ZEROINIT
GHND GMEM_MOVEABLE + GMEM_ZEROINIT

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

function GetHeapStatus(): TheapStatus;

Узнать текущее состояние диспетчера памяти.
Структура TheapStatus:

THeapStatus = record // Размер памяти в байтах которая доступна программе TotalAddrSpace: Cardinal; // Сколько памяти из TotalAddrSpace не находятся в SWAPе TotalUncommitted: Cardinal; // Сколько памяти из TotalAddrSpace находятся в SWAPе TotalCommitted: Cardinal; // Сколько всего динамической памяти выделено программе TotalAllocated: Cardinal; // Сколько памяти еще доступно для выделения (увеличивается) TotalFree: Cardinal; // Сколько памяти доступно в маленьких блоках FreeSmall: Cardinal; // Сколько памяти доступно в больших блоках // непрерывно идущие маленькие блоки могут складываться FreeBig: Cardinal; // Доступная, но еще не выделявшаяся память Unused: Cardinal; // Размер памяти используемой для нужд менеджера Overhead: Cardinal; // Внутренний статус кучи HeapErrorCode: Cardinal; end;

Если используется SharedMem, то статус относится к куче разделяемой несколькими процессами.

Последнее изменение этой страницы: 2020-02-21; Нарушение авторского права страницы

GetMem — Функция Delphi

Существует много различных функций выделения памяти, как встроеных в Делфи так и виндовских.
Какие из них лучше использовать, если необходимо передвать паметры между программой и программой, программой и ддлкой?

Функции GetMem, VitruaAlloc, ReallocMem и тд.

spider13, вспомни, как ты отвечаешь другим, и получи аналогичный ответ: В интернете полно информации о GetMem, VirtualAlloc и ReallocMem. Ищи здесь

(напомнить тебе, где ты отвечал в таком стиле?)

Функции и процедуры для работы с памятью и указателями

Addr Возвращает указатель на объект.
AllocMem Выделяет на куче блок памяти заданного размера, заполняет его нулями и возвращает указатель на начало блока.
CompareMem Выполняет бинарное сравнение двух участков памяти.
GetHeapStatus Возвращает текущее состояние диспетчера памяти.
GetMemoryManager Возвращает значения указателей полей текущего диспетчера памяти.
IsMemoryManagerSet Определяет, используется в настоящий момент диспетчер памяти, установленный по умолчанию, или был установлен другой диспетчер.
Ptr Возвращает указатель на адрес памяти, переданный в качестве аргумента.
SizeOf Возвращает размер памяти, занимаемый переменной.
SetMemoryManager Устанавливает значения полей диспетчера памяти.
SysFreeMem Высвобождает память, используемую динамической переменной.
SysGetMem Выделяет блок памяти заданного размера и возвращает указатель на него.
SysReallocMem Изменяет размер динамически распределенного блока памяти.

Процедуры для работы с динамическими переменными

Dispose Высвобождает память из-под динамической переменной.
Finalize Деинициализирует динамическую переменную.
FreeMem Высвобождает память из-под динамической переменной.
GetMem Создает динамическую переменную, выделяя под нее указанный объем памяти.
Initialize Инициализирует динамическую переменную.
New Создает динамическую переменную.
ReallocMem Перераспределяет память для динамической переменной.

———————————————————————————
Функция AllocMem( Size: Cardinal ): Poiner;
Модуль: SysUtils
Описание: Функция выделяет на куче блок памяти размером Size байт, заполняет его нулями и возвращает указатель на начало блока.
———————————————————————————
Функция SizeOf( X ): Integer;
Модуль: System
Описание: Функция возвращает размер памяти, которую занимает переменная X в байтах. Результат функции зависит только от типа переменной X и не зависит от ее значения. Данную функцию удобно использовать совместно с процедурами FillChar, Move, и GetMem.
———————————————————————————
Процедура GetMem( var P: Pointer; Size: Integer );
Модуль: System
Описание: Процедура создает динамическую переменную: выделяет блок памяти размером Size байт под переменную, указанную в параметре P, и возвращает указатель на начало данного блока памяти. Параметр P может представлять собой любой тип указателя. Указатель на новую созданную переменную записывается как P^. Если для создания динамической переменной недостаточно памяти, то возникает исключение EOutOfMemory.
———————————————————————————
Процедура FreeMem( var P: Pointer [; Size: Integer] );
Модуль: System
Описание: Процедура уничтожает переменную, с которой связан указатель P и высвобождает память, занимаемую данной переменной. В необязательном параметре Size указывается объем памяти в байтах, выделенный ранее динамически под переменную. Если после действия процедуры FreeMem, вызвать указатель P, то возникнет ошибка, т.к. указатель имеет неопределенное значение.
———————————————————————————
Процедура ReallocMem (
var P: Pointer;
Size: Integer );
Модуль: System
Описание:
Процедура перераспределяет память размером Size байт под динамическую переменную P.
При вызове данной процедуры указатель P должен иметь значение nil или должен указывать на динамическую переменную, память под которую была предварительно выделена с помощью процедур GetMem или ReallocMem.
Если P = nil, Size = 0, то процедура не производит никаких действий.
Если P = nil, а Size <> 0, то процедура распределяет новый блок памяти размером Size и устанавливает указатель P на начало блока. Такой вызов процедуры аналогичен обращению к процедуре GetMem.
Если P <> nil, а Size = 0, то процедура высвобождает блок памяти, на который указывает P и устанавливает P = nil. Вызов процедуры с указанными параметрами аналогичен обращению к процедуре FreeMem, но в отличие от FreeMem процедура ReallocMem очищает указатель.
Если P <> nil и Size <> 0, то процедура устанавливает размер блока памяти, выделенный ранее под динамическую переменную P, равным Size. При этом существующие данные сохраняются. Если размер блока памяти будет увеличен в размерах, то данные в новой части блока будут неопределенными. Если новый размер блока памяти не может быть выделен в текущем адресном пространстве, то он перемещается на новое место и соответственно параметр P будет указывать на новый участок памяти.

Подробный формат команды можно увидель в Delphi нажав F1 или держа на Ctrl кликнуть на функцию.

Блог GunSmoker-а

. when altering one’s mind becomes as easy as programming a computer, what does it mean to be human.

18 июня 2011 г.

Адресное пространство под микроскопом

Продолжаем тему про память в программах Delphi.

В прошлый раз мы узнали о том, что понятие «память» в программах представляет собой большой однородный массив байтов, где и хранится в куче вообще всё, с чем напрямую работает программа. Массив этот виртуален, не ограничен количеством физической памяти, индивидуален у каждой программы и достаточно велик, чтобы вы не задумывались о его ограничениях 99% времени. Большой размер, изолированность и однородность существенно упрощает разработку программ.

Ну, вообще-то я вам наврал. Извините, если вы мне поверили.

На самом деле, этот большой массив не так уж однороден, как я пытался это показать в прошлый раз (хотя какие-то намёки я и делал). В нём выделяется много зон с различной смысловой нагрузкой, называемых регионами. К рассмотрению которых мы сейчас и приступим.

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

Достаточно пёстрая картина, не так-ли?

Основные регионы

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

  • Виртуальная память
    1. Виртуальное адресное пространство
      1. Режим пользователя
        1. Область для отлова нулевых указателей
        2. Зарезервированный блок в 64 Кб на границе 2 Гб
        3. Раздел для кода и данных программы
          1. Код
          2. Данные
            1. Проецируемые в память файлы
              1. Частные
                1. Именованные
                2. Безымянные
              2. Разделяемые
                1. Именованные
                2. Безымянные
            2. Основная рабочая виртуальная память
              1. Частная память
                1. Стек
                2. Куча
                3. Прочая (динамическая память)
              2. Разделяемая память
          3. Недоступная память
            1. Потерянная память
            2. Свободная память
      2. Режим ядра
    2. Виртуальная память, не имеющая прямого отображения в адресном пространстве

Сразу предупрежу, что эта схема — просто один из способов классификации различных зон внутри виртуального адресного пространства. Она не является ни точной (к примеру, не показана/не раскрыта область режима ядра), ни абсолютной (вы можете сгруппировать классификацию и по-другому — к примеру, поделить проецируемые файлы сначала по доступности, а потом по именованности; или вообще вынести «разделяемость» на уровень выше, захватив заодно и секции кода). Также на этой схеме вообще никак не отображено отношение между физической и виртуальной памятью. Тем не менее, эта схема может пригодиться как отправная точка.

Секции кода, секции данных

Пожалуй, первое, что бросается в глаза — мы видим в «памяти программы» саму программу! (А также все библиотеки, которые она использует). Эта зона в VMMap называется Image и она помечена фиолетовым цветом. Несложно сообразить, почему это так. «Память программы», хотя теперь и виртуальна, является эмуляцией физической памяти (RAM, оперативной памяти). Процессор не может выполнять машинный код на внешней памяти (дисках, флешках) — он может обрабатывать только код в оперативной памяти. Поскольку виртуальная память является эмуляцией физической, то отсюда напрямую следует, что программа должна быть загружена в виртуальное адресное пространство, чтобы её можно было выполнить.

Программа появляется в адресном пространстве с помощью механизма проецирования файлов. Exe-файл проецируется на адресное пространство программы и становится его частью. Любое обращение к памяти по этим адресам приведёт к фактическому чтению данных с диска. С точки зрения программы эта часть выглядит как обычная «память», ничем не отличающаяся от любой другой памяти. Но на самом деле эта память читается из файла на диске.

Тут надо сделать замечание, что «обычная» виртуальная память так же может читать из файла при обращении к ней. К примеру, как мы помним, при недостатке физической памяти, часть виртуальной памяти может быть выгружена из физической памяти на диск — в файл подкачки (т.н. страничный файл, paged file).

Вот пример загруженного Total Commander:

Как вы можете видеть, весь 3.5 Мб .exe файл при загрузке в память оказался поделенным на части. Причём у всех частей оказался разный способ доступа. Это связано с тем, что в .exe файле хранятся разные вещи: код, константы, ресурсы, данные. Соответственно, они и обрабатываются по-разному. Я уже упоминал о том, что на уровне ВУ языка данные характеризуются именем-типом-семантикой, а на уровне процессора — адресом, размером и атрибутами доступа. Вот сейчас мы видим, что собственно исполняемый код Total Commander оказался размером в 2.8 Мб и имеет атрибуты доступа «чтение и выполнение» (на рисунке он выделен). Непосредственно перед ним идёт PE-заголовок .exe файла, имеющий доступ только для чтения. Заголовок программы содержит информацию о ней — сколько в ней кода, сколько данных, какие требования для её выполнения (вроде версии ОС, атрибутов запуска вроде флагов LargeAddressAware и т.п.). Всё, что идёт после секции кода является различными данными. Соответственно, у секций данных доступ либо только на чтение, либо на чтение/запись. Их мы рассмотрим чуть позже. Исключением (с атрибутом копирования при записи) является секция .idata, которая имеет специальное назначение — это вспомогательный блок для оптимизации статического импорта функций из библиотек. Но это не тема этой статьи.

Атрибуты защиты (доступа) выставляются на уровне страниц памяти (размер страницы памяти — 4 Кб). Соблюдение режима доступа контролируется аппаратно, самим процессором. Если вы попробуете выполнить недопустимую операцию (вроде записи по адресу, принадлежащему странице с атрибутом доступа «чтение»), то процессор возбудит исключение нарушения доступа (Access Violation). Кстати, это (доступ к странице памяти с неверным доступом) — единственная причина для возникновения этого исключения.

Атрибуты защиты в чём-то похожи на аппаратное разделение на режим ядра и режим пользователя — в том смысле, что они предоставляют защиту от ошибок (непреднамеренного/ошибочного доступа «не туда»). И защита эта выполняется на аппаратном уровне. Хотя, конечно, в отличие от разделения на режимы ядра и пользователя, вы можете менять атрибуты защиты страницы.

Всего есть три атрибута — чтение, запись и выполнение, которые могут комбинироваться в любой последовательности (отсутствие атрибутов можно считать как атрибут «no access» — PAGE_NOACCESS), а также два специальных модификатора — copy-on-write и guard (будут рассмотрены ниже). Секции данных обычно имеют доступ на чтение (константы), чтение/запись (переменные), а секции кода — на чтение и выполнение. Иногда вы можете генерировать код на лету. В этом случае вы должны добавлять атрибут «выполнения» (и, предпочтительно, снять атрибут «записи») к памяти, в которой хранится сгенерированный код, перед его выполнением. Иначе вы схлопочете свой законный вылет — нельзя запустить код, который не имеет атрибута «выполнение» (а только «чтение»).

Примечание: на ранних процессорах архитектуры x86 атрибуты «чтение» и «выполнение» были эквивалентными. Поэтому, хотя технически ставить равенство между ними никогда не было верным, некоторые воспользовались этой особенностью реализации и передавали управление на код в сегментах данных. Повторим: это никогда не было корректным, но это работало на старых процессорах. Теперь этот код будет вылетать. См. также: DEP.

Проецируемые файлы

Помимо самой программы и её библиотек в адресном пространстве программы проецируемые файлы могут присутствовать и как рабочие объекты. Их может создавать и использовать код программы. В VMMap эти регионы называются Mapped Files и маркированы синим цветом.

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

Операции с файлами — это то, что рано или поздно приходится делать практически во всех программах, и всегда это вызывает массу проблем. Должно ли приложение просто открыть файл, считать и закрыть его, или открыть, считать фрагмент в буфер и перезаписать его в другую часть файла? В Windows многие из этих проблем решаются очень изящно — с помощью проецируемых в память файлов (memory-mapped files).

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

Кроме этого, проецируемый в память файл может иметь имя и быть разделяемым, т.е. совместно использоваться двумя и более программами. Это удобный механизм обмена данными между двумя процессами (IPC) на базе которого построены почти все прочие механизмы межпроцессного обмена данными. Вспомните, что виртуальное адресное пространство у каждой программы своё, оно изолировано. Иными словами:
Этот код будет делать всяческие плохие вещи (вылет с Access Violation, вылет программы, чтение мусора), а не то, что вы ожидаете (чтение данных из другой программы), потому что вы не приняли во внимание изолированность программ. В вашей программе по этому адресу лежит что-то совершенно иное. Это примерно как на каждой улице (программе) есть дом номер 3 (адрес памяти). Вы не можете сказать «дом 3» и надеяться, что письмо дойдёт. Более того, даже хотя вы можете как-бы «приписать» к «дом 3» «название улицы» (ID процесса) — с помощью функций вроде ReadProcessMemory и WriteProcessMemory — это не является рекомендуемым способом обмена данными. А вот разделяемые проецируемые в память файлы (и любые другие механизмы IPC, которые, так или иначе, на них основаны) как раз и являются тем, что вам нужно использовать, если вы хотите организовать взаимодействие между двумя программами.

Частная и разделяемая память

Итак, за вычетом проецируемых файлов (которые делятся на собственно проецируемые файлы и загруженные исполняемые модули), у нас остаётся «основная масса памяти». Большую часть из неё занимают обычные данные, которые индивидуальны для нашей программы. Это т.н. private (частные) данные. В VMMap они выделены жёлтым цветом (надо иметь в виду, что VMMap также выделяет часть private-данных в отдельные области — см. ниже). Эти данные используются только нашей программой и не имеют отображения в других программах. Т.е. это самая типичная память программы. Кроме частных данных в адресном пространстве есть и т.н. shareable (разделяемые) данные. Это специальным образом оформленные секции DLL-файлов, которые совместно используются несколькими программами. Delphi не поддерживает создание подобных секций, так что мы не будем подробно рассматривать их. Следует также заметить, что некоторые проецируемые в память файлы также могут совместно использоваться несколькими программами. Кроме того, совместно используются и любые секции кода (за исключением ситуаций, когда их модифицируют при выполнении): если запущено 10 экземпляров программы, то нет смысла держать в памяти 10 копий .exe файла. Поэтому, хотя в системе будет 10 различных виртуальных адресных пространств, и во всех них будет 10 .exe файлов, на самом деле, все эти .exe файлы будут одним и тем же файлом, спроецированным на вcе адресные пространства. Т.е. вместо 10 копий в оперативной памяти будет всего одна.

Потерянная и свободная память

Это все основные регионы, которые можно использовать. Вся прочая память является недоступной. Любое обращение к ней приводит к возбуждению исключения нарушения доступа (Access Violation) — это частный случай неправильного доступа памяти (к примеру, попытка чтения данных со страницы памяти с атрибутом доступа PAGE_NOACCESS). Но даже недоступная память делится на две категории. Ну, самое очевидное — свободная память. Это память, которая ещё не занята. Такой у нас большинство. В VMMap это белые блоки (free memory). Кроме свободной памяти в недоступной памяти числится память, которая не может быть использована из-за фрагментации (серый цвет в VMMap, unusable). Вспомним, что память выделяется кусками по 64 Кб, но гранулярность блока памяти — размер страницы (что есть 4 Кб). Это означает, что если вы выделите 2 Кб, то вам выделят 4 Кб, а 60 Кб окажутся навеки недоступными — ну, пока вы не освободите эти ваши 2 Кб (на самом деле — 4 Кб) памяти, так что весь блок в 64 Кб не станет вновь целиком доступен для использования.

Классификация основной рабочей памяти

Фух, выше было сказано ужасно много всего. Но, кажется, это было довольно далеко от простого var I: Integer , не так ли? Да, довольно далеко. Но это было необходимо, чтобы протянуть мостик между адресным пространством (системным понятием) и var I: Integer (понятием ВУ языка). Теперь, когда мы немного их сблизили, настало время поговорить «поближе к Delphi». Начнём, пожалуй, с самого простого, что только может быть в вашей программе:

Константы

Константы — это, пожалуй, самое простое, что может быть в программе. Но даже они трактуются аж тремя различными способами. И, нет, локальные и глобальные константы ничем друг от друга не отличаются на уровне машинного языка. На низком уровне все константы в программе являются глобальными. Ограничение локальных констант существует только в семантике языка ВУ.

Один из способов — это устаревшее использование констант. Т.н. режим «writable const» (опция <$J+>/ <$J->). Это режим, при котором константа фактически трактуется как переменная. В современной Delphi не имеет смысла, кроме обратной совместимости с (очень) старым кодом.

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

Более сложные константы, которые имеют большой размер, обрабатываются иначе. Все они записываются в один большой блок — сегмент данных, который записывается в .exe файл после секции кода. Генерируемый машинный код (помещаемый, конечно же, в секцию кода) обращается (указывает) на данные в секции данных — туда, куда компилятор поместил константу.

Таким образом, константы появляются в памяти автоматически, вам ничего и никогда не нужно для этого делать — если программа загрузилась в память, то вместе с ней загружены и константы. Но это не значит, что об управлении памятью вам придётся навсегда забыть. К примеру, если вы загрузили DLL, вызвали функцию в ней, она вернула вам указатель на данные-константу (к примеру, её GUID), а затем вы выгрузили DLL и пытаетесь обратиться к данным по указателю — то вы, конечно же, получите свой законный Access Violation, потому что страницы памяти, которые некогда занимала константа, после выгрузки DLL оказываются свободными (имеют атрибут PAGE_NOACCESS).

Кстати, как паковать константы вместе и как их хранить — сугубо деталь реализации компилятора. Далеко не факт, что данные константы будут иметь атрибут защиты «только чтение». Да, «только чтение» — это то, что мы могли бы ожидать, и что нам хотелось бы. Но на практике компилятор может помещать константы в один блок с другими данными (которые имеют доступ чтение/запись), так что разграничить их не представляется возможным. Впрочем, мне пока не встречалось ни одной ситуации, где бы это имело значение.

Глобальные переменные

Глобальные переменные, хотя и могут не только читаться, но и писаться (в сравнении с константами), на деле являются ещё более простыми вещами, поскольку трактуются всего одним образом. И способ этот очень похож на хранение сложных констант. Все глобальные переменные складируются в одну кучу, в один блок памяти. Но в отличие от констант, глобальные переменные (обычно) не сохраняются в .exe файле. Вместо этого в .exe файле создаётся специальная секция «инициализация нулями при загрузке». Указывается только размер. При загрузке .exe файла в память ОС выделит пустой блок памяти, заполненный нулевыми байтами нужного размера — это и будет область для наших глобальных переменных (кстати, на рисунке с Total Commander-ом выше эта область называется BSS). Таким образом, все глобальные переменные инициализируются нулями — это механизм не языка, а ОС (причём аналогичный механизм существует во многих других системах).

Но что же насчёт инициализированных глобальных переменных, спросите вы? Ну, тут может быть несколько подходов, но самый типичный — выделение инициализированных глобальных переменных в отдельный блок памяти. Все инициализированные переменные сохраняются в сегмент данных. Он помещается в .exe файл (обычно — в сегмент с именем DATA). При загрузке программы этот сегмент загружается в память и получает доступ на чтение/запись. Некоторые компиляторы складируют в этот блок (DATA) как инициализированные глобальные переменные, так и константы.

Кстати, тут нужно сделать замечание такого плана: в языке Delphi есть несколько вещей, для которых не указаны точно правила инициализации, но которые по своей сути являются глобальными. К примеру — классовые переменные. Аналогично, равно как на уровне машинного языка нет разницы между локальными и глобальными константами, ровно так же на уровне машинного языка нет разницы между глобальными переменными и (в этом примере) классовыми переменными. Поэтому, зная то, как глобальные переменные инициализируются под капотом языка, вы можете сделать обоснованное предположение, что это — недостаток документации (не дописана/не упоминает этот момент), и что классовые переменные гарантировано обнулены при старте программы.

Собственно, замечания по управлению памятью те же, что и для констант: глобальные переменные доступны всегда, до тех пор, пока в память загружен «их» исполняемый модуль. Вам ничего не нужно делать для управления ими.

Ресурсы

Если вы внимательно дочитали до этого момента, то вы уже догадываетесь, что я хочу здесь сказать: да, ресурсы в .exe файле работают практически как константы: они собраны в единую секцию (.rsrc), которая доступна в памяти одновременно с наличием там самого исполняемого файла. Вы можете получать к ним доступ как к любым другим данным. Конечно же, по-хорошему, вам нужно использовать кучу функций для этого — вроде LoadResource и LockResource , но надо понимать, что эти функции — наследие старых времён, когда загрузка файла в память (да и работа с памятью вообще) выполнялась существенно иначе. В современном мире плоской модели памяти эти функции существуют только для обратной совместимости.

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

Динамическая память

Хотя мне очень хочется перейти уже наконец-то к нашему простому var I: Integer , мне кажется, что будет лучше дать сперва более простой материал: динамическую память и кучу.

Динамическая память — это память, получающаяся из свободной памяти при выделении памяти. Слово «динамическая» здесь означает «динамически распределяемая». Т.е. в отличие от статической памяти (которая постоянна на длительных участках выполнения программы), динамическая память характеризуется своим непостоянством — она выделяется и освобождается во время выполнения кода программы. Статическая же память выделяется при загрузке программы (модуля) и освобождается при её выгрузке. Динамическая память используется когда вы не знаете размер данных и/или их количество на этапе кодирования. К примеру: какого размера будет файл, сколько в нём будет строк и т.п. В этом случае вы не выделяете никаких ресурсов в программе изначально, но вместо этого вы выделяете память по необходимости уже во время выполнения программы.

Динамическая память выделяется и освобождается функциями VirtualAllocEx и VirtualFreeEx (а также любыми их аналогами или обёртками вроде VirtualAlloc / VirtualFree ).

Собственно здесь всё достаточно просто и прямолинейно: вы вызываете VirtualAlloc ( Ex ), указывая размер блока памяти и желаемый атрибут доступа (обычно: чтение-запись). Система откусывает от свободной памяти блок, округлённый до «нужных кратностей» и отдаёт его вам. Теперь у вас в программе выделена память, а на руках у вас есть указатель — делайте с ним что угодно. Когда память надо освободить — вызывайте VirtualFree . Система переведёт память обратно в свободную. Достаточно прозрачно.

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

Как я уже не раз указывал: выделение памяти напрямую через системные функции — не самая удачная идея. Поэтому обычно в программе для управления памятью используют другой механизм: кучу (heap).

Иногда термин «куча» употребляют как синоним термина «динамическая память». Это имеет смысл с точки зрения строгой терминологии: ведь куча (по определению) — это название структуры данных, с помощью которой реализована динамически распределяемая память приложения.

С другой стороны, термин «куча» употребляется и в другом смысле — когда ссылаются на вполне конкретный механизм (например: системную кучу процесса или любой другой менеджер памяти). Поэтому вам нужно выделять контекст разговора и каждый раз понимать, что конкретно понимается под «кучей».

Для удобства, здесь и далее я буду полагать такую интерпретацию: любая куча является динамической памятью. Но не любая динамическая память — куча. Память, выделяемую через VirtualAlloc ( Ex ), будем называть «динамической памятью», но не «кучей». А под «кучей» будем понимать дополнительный код по управлению памяти, реализованный «поверх» динамической памяти: т.н. менеджер памяти или диспетчер кучи.

Такая интерпретация имеет смысл: ведь смысл кучи сводится к обработке множества запросов на создание/разрушение множества мелких объектов (блоков памяти). Очевидно, что «голая» динамическая память (через VirtualAlloc ( Ex )) крайне плохо приспособлена к этой задаче (из-за ограничений на кратность выделения и размера). А вот различные менеджеры памяти как раз и созданы для решения этой задачи. Так что к ним как нельзя лучше подходит термин «куча».

Именно куча (через менеджер памяти Delphi) является стандартом для работы с динамической памятью в Delphi. Все запросы на создание объектов, выделения памяти идут через стандартный менеджер памяти Delphi. Крайне редко при этом используются иные механизмы.

Менеджер памяти в Delphi представлен функциями GetMem , FreeMem и ReallocMem . Все прочие функции ( New / Dispose , AllocMem , SetLength , TObject.Create и т.п.) являются лишь обёртками к этим низкоуровневым функциям менеджера памяти Delphi. Эти обёртки удобнее использовать, чем функции менеджера памяти, потому что они выполняют некоторую вспомогательную работу (самая типичная — вычисление размера в байтах по типу переменной и количеству элементов). Использование GetMem и FreeMem очень похоже на использование VirtualAlloc и VirtualFree — разница только в меньшем объёме функций (к примеру, память всегда выделяется только на чтение/запись), да в том, что память берётся не напрямую из системы, а из предварительно выделенной памяти.

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

Как уже было сказано, куча весьма удобна при создании множества мелких блоков данных. Например, связанными списками и деревьями проще манипулировать, используя именно кучу, а не динамическую память. Преимущество кучи в том, что она позволяет вам игнорировать гранулярность выделения памяти и размер страниц и сосредоточиться непосредственно на своей задаче — т.е. абстрагироваться от особенностей железа (аппаратной части). А недостаток является прямым следствием преимущества: вы теряете прямой контроль над передачей физической памяти и её возвратом системе. Что касается скорости работы, то она сильно зависит от специфики данных. Как правило, менеджер памяти оптимизируется под типичные операции в программе и работа с ним наиболее оптимальна в большинстве случаев. С другой стороны, могут быть вырожденные случаи, когда накладные расходы на управление памяти в куче перевешивают выигрыш от экономии на вызовах ядра (вызов VirtualAlloc ( Ex ) означает обращение к режиму ядра, а переключение контекстов (пользовательский режим -> режим ядра и обратно) — не самая быстрая операция).

Далее надо сказать, что стандартный менеджер памяти Delphi — это не единственный диспетчер кучи в вашей программе. Вам также доступны (как минимум) эти варианты:

  • HeapAlloc / HeapFree
  • LocalAlloc / LocalFree
  • GlobalAlloc / GlobalFree
  • IMalloc.Alloc / IMalloc.Free
  • CoTaskMemAlloc / CoTaskMemFree
  • SHAlloc / SHFree

Чем они отличаются? Да мало чем. Это разные реализации одной идеи. В системе есть несколько менеджеров памяти, которые имеют разные цели и используются в разных случаях. Вышеуказанные функции — это точки доступа к различным менеджерам памяти (которые, кстати, не имеют никакого отношения к Delphi). Когда их надо использовать? Когда вам нужно «поговорить» с другим кодом, который понимает только их. К примеру, буфер обмена Windows работает с GlobalAlloc / GlobalFree . А в остальных случаях (т.е. почти всегда) вам нужно использовать менеджер памяти Delphi.

Самое главное, что тут нужно знать — это правило: «кто память выделил — тот её и должен освобождать». Иными словами, если вы выделили память через GetMem — то и освобождать её должны через FreeMem . Если память выделена через VirtualAlloc , то освобождать её надо через VirtualFree . Если выделена через LocalAlloc — то LocalFree . И так далее. Нельзя смешивать одно с другим.

Да, кстати, VMMap показывает и кучу и динамическую память одинаково — как «Private Data» (жёлтым цветом). А вот системную кучу процесса ( HeapAlloc / HeapFree ) она показывает отдельно — как «Heap (Private Data)» (красный цвет).

Ну, наконец-то мы добрались до самого конца: нашего долгожданного var I: Integer . Локально объявленные переменные простых (не динамических) типов называются «статическими». Следующий (бессмысленный) пример кода демонстрирует объявление всех выше обсуждаемых :
Для начала заметим такую простую вещь: хотя в данном примере GV4-6 и LV4-6 — динамические и выделяют память под свои данные в куче, но сами переменные (все из которых являются 4-х байтовыми указателями на x86-32 и 8-ми байтовыми указателями на x86-64) являются статическими. Иными словами, у чисто статических переменных «переменная» = «данные». У динамических переменных «переменная» <> «данные» — у них «переменная» = «указатель», а «данные» находятся по этому указателю. Кроме того, хотя в этом примере все 6 динамических переменных оказались «статическими» (как собственно переменные, а не данные) — но в общем случае это может быть не так: к примеру, переменная может быть и полем объекта — и, следовательно, выделяться в куче. Например:
Здесь Next может располагаться как статически (если вы просто объявите глобальную переменную типа TTest ), так и динамически — если вы выделите память под TTest через GetMem / AllocMem / New (получив на руки указатель PTest ).

Но это было лирическое отступление. Итак, если вы внимательно посмотрите на код выше, то заметите две странных вещи:

  1. Локальные переменные не инициализируются нулями
  2. Локальным переменным нельзя задать значение при объявлении

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

В коде отсутствуют операции выделения/освобождения памяти (вроде GetMem / FreeMem ), но локальные переменные автоматически доступны — значит, можно предположить, что они не размещаются ни в куче, ни в динамической памяти. А где тогда?

В адресном пространстве любого процесса есть как минимум один очень специальный регион памяти, называемый «стеком потока». Всякий раз, когда в процессе создается поток, система резервирует регион адресного пространства для стека потока (у каждого потока свой стек) и передает этому региону какой-то объем физической памяти. По умолчанию система резервирует 1 Мб адресного пространства и передает ему всего две страницы памяти (размер резервируемого региона по умолчанию указывается в заголовке PE файла; он так же может явно указываться вызывающим при создании потока).

Именно в стеке потока и хранятся локальные переменные.

Но это ничего не объясняет. Почему «стек»? И разве он не в динамической памяти выделяется?

Давайте по порядку.

Итак, почему — «стек»?

«Стек» (англ. «stack» — стопка) — это структура данных, в которой доступ к элементам организован по принципу LIFO (англ. last in — first out, «последним пришёл — первым вышел»). Чаще всего принцип работы стека сравнивают со стопкой тарелок: чтобы взять вторую сверху, нужно снять верхнюю:

Операция помещения элемента в стек называется «заталкиванием» (push), а обратная к ней — «выталкиванием» (pop). Обе операции возможны только по отношению к верхушке стека.

Хорошо, а какое это имеет отношение к локальным переменным?

Вспомните, что локальные переменные — «свои» у каждой функции и процедуры:
Более того: эти переменные — «свои» у каждого вызова одной и той же функции:
Замечаете сходство? Функции и процедуры выполняются так же, как заносятся элементы в стек. Вы вызываете процедуру — это аналог «помещения процедуры в стек». Процедура закончила выполнение и возвращает выполнение — это аналог «извлечения процедуры из стека». Вы всегда работаете с вершиной стека:

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

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

Кстати, стек растёт «в обратную сторону»: от старших (бОльших) адресов в сторону младших (меньших). В частности, на рисунке выше 0 ( nil ), будь он показан, располагался бы где-то сверху. А граница в 4 Гб (16 Эб для x86-64) — где-то снизу.

Хорошо, со «стеком» более-менее понятно — но почему тогда «статические»? Вроде описанный механизм сильно напоминает динамический? Ведь память выделяется и освобождается в процессе работы программы.

Действительно, статические локальные переменные — это не то же самое, что статические глобальные переменные. Последние статичны «в более широком смысле»: они доступны, пока загружен исполняемый модуль (.exe или .DLL). А локальные переменные существуют только во время выполнения функции. Когда функция возвращает управление локальные переменные уже не существуют.

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

Тем не менее, локальные переменные называются «статическими» по следующим соображениям:

  1. В коде вызова процедуры или функции нет кода выделения динамической памяти. Его нет даже под капотом языка. Хотя надо сказать, что вызываемый выделяет память сам (на уровне машинного кода). Но делает это он сдвигом вершины стека, а не обращением к VirtualAlloc . Более того, стек не является прямой обёрткой к VirtualAlloc — вся память уже выделена, машинный код просто меняет «данные учёта», сдвигая границу вершины стека.
  2. Стек управляется аппаратно — про него (в определённой степени) «в курсе» центральный процессор. Т.е. если все ранее/выше обсуждаемые регионы для процессора были равнозначны (единственное, что его заботит — атрибуты защиты страниц памяти, да разделение на режим ядра и пользователя), то для работы со стеком у процессора есть специальные регистры и команды.
  3. Локальные переменные существуют в течение всего времени их «контейнера». Контейнером глобальных переменных является исполняемый модуль. Контейнером локальных переменных является функция или процедура. Глобальные переменные существуют, пока существует исполняемый модуль. Поэтому — они статичны. Локальные переменные существуют, пока существует (выполняющаяся) функция. Именно поэтому они — статичны (по аналогии): потому что они живут в течение всего времени жизни контейнера (и им не нужно ручное выделение/освобождение памяти). Сравните это с динамическими переменными, которые могут:
    1. Жить короче времени жизни контейнера — вы можете создать и удалить динамические данные только на коротком участке кода процедуры; на прочем коде процедуры динамическая переменная (её данные) не будет существовать.
    2. Жить «многократно» — вы можете многократно создавать и удалять одну или несколько динамических переменных в одной и той же процедуре.
    3. Жить после «смерти» контейнера — вы можете создать динамические данные и передать их вызывающему. Заметьте, что вы не можете сделать это со статическими переменными (но вы можете дать вызывающему их скопировать).

Ладно, вроде со «статическим» понятно: данные статичны «локально» — на время работы функции. Но что насчёт выделения памяти: откуда она берётся?

Как я уже сказал выше: система резервирует под стек потока регион и выделяет в нём лишь две страницы памяти, а также устанавливает указатель стека на его вершину — на конец страницы, с которой поток начнет использовать свой стек. Вторая страница сверху называется сторожевой (guard page) и имеет специальный атрибут защиты — PAGE_GUARD.

По мере разрастания дерева вызовов (одновременного обращения ко всё большему числу функций) потоку, естественно, требуется и больший объем стека. Как только поток обращается к следующей странице (а она является сторожевой), то процессор возбуждает аппаратное исключение «доступ к сторожевой странице» (это не механизм, специфичный для стека, а вообще для любой страницы с PAGE_GUARD-атрибутом). Система видит это исключение и расширяет стек — та страница, что была сторожевой, становится обычной read-write страницей, а следующая за ней (в сторону младших адресов, т.к. стек растёт в обратную сторону) становится новой сторожевой страницей. Благодаря такому механизму работы, объем памяти, занимаемой стеком, увеличивается только по мере необходимости.

Понятно, что бесконечно стек потока расти не может — рано или поздно он упрётся в ограничение (занятую страницу памяти). В этом случае, когда система видит, что стек расширять уже некуда, она возбуждает исключение EXCEPTION_STACK_OVERFLOW (одновременно передав память под последнюю страницу памяти; страницы с PAGE_GUARD более не будет). Если же ваш поток продолжит использовать стек даже после исключения, связанного с переполнением стека (т.е. исчерпает последние 4 Кб из последней же страницы памяти), то при очередной попытке добавления данных в стек поток выйдет за пределы стека и наткнётся на страницу с доступом PAGE_NOACCESS. Такая страница, память которой никогда не передаётся, всегда ограничивает стек сверху. Поскольку это обычная страница без доступа, то будет возбуждено стандартное исключение Access Violation. Вероятнее всего, вы не сможете его обработать (ведь обработка требует стека). Поэтому ваш процесс будет аварийно завершён.

Кстати говоря, стек умеет только расширяться. Однажды увеличив свой размер, он более не уменьшается. И (ещё «кстати») стек в VMMap показан оранжевым цветом (регионы «Stack»).

Библиотека RTL Delphi содержит функцию (точнее — «магию компилятора»), позволяющую контролировать стек. Она обеспечивает корректную передачу страниц физической памяти стеку потока. Возьмем, к примеру, небольшую функцию, требующую массу памяти под свои локальные переменные:
Для размещения массива Buffer функция потребует минимум 10 Кб стекового пространства. В системе с размером страниц по 4 Кб это могло бы создать проблему. Если первое обращение к стеку проходит по адресу, расположенному ниже сторожевой страницы (как в показанном выше фрагменте кода), то поток обратится к зарезервированной памяти, и возникнет нарушение доступа (вместо обращения к сторожевой странице и расширения стека). Поэтому, чтобы можно было спокойно писать функции, вроде приведенной выше, компилятор и вставляет в генерируемый код специальную конструкцию для контроля стека. Этот код представляет просто цикл, который проходится по стеку с крупным шагом (в страницу) по области текущей функции, «касаясь» (читая) данных стека. В процессе такого «касания» может быть задета сторожевая страница и стек «подрастёт» достаточно, чтобы вместить все локальные данные процедуры.

И под конец замечу, что стек выделяется из свободного виртуального адресного пространства системой при создании потока. Хотя это очень похоже на динамическую память (и вы при желании можете руками его освободить или ещё как-то «напакостить» — как и с любой другой динамической памятью), но обычно стек не называют динамической памятью — по тем же соображениям, по которым локальные переменные называют «статическими»: потому что стек выделяется системой автоматически, единожды, при создании потока и до его успешного завершения (стек не удаляется при насильном уничтожении потока).

Прочие регионы

Осталось только упомянуть, что память в адресном пространстве выделяете не только вы (ваша программа), но и система. К примеру, система хранит блоки переменных окружения процесса и его потоков. Другой пример — работа с любыми «не ядерными» объектами. К примеру, обращение к подсистемам User и GDI может выделять память в адресном пространстве процесса для хранения служебных данных объектов User или GDI. Тут важно понимать, что в этом случае система ничем не отличается от вашей программы — она ведёт себя точно так же как вы. Она является как бы составной частью вашей программы. Это в том смысле, что система не использует какие-то «секретные места», а выделяет память в динамической памяти или куче — ровно как и вы (конечно же, система не использует менеджер памяти Delphi — она использует какой-то из своих менеджеров). Т.е. она выступает как простой набор сторонних библиотек, ничем не отличающихся от любых других библиотек сторонних производителей (или даже ваших DLL).

Прочие важные понятия

Статические и динамические данные

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

  • Автоматическое управление памятью: всегда доступны на время жизни контейнера.
  • Имеют фиксированный размер.
  • Имеют фиксированное количество.
  • Могут иметь ограничения на размер (локальные данные не могут быть больше размера стека).
  • Не могут генерировать утечки памяти (поскольку уходят вместе с контейнером).
  • Переменная = данные.
  • Могут быть локальными и глобальными.

Динамические:

  • Ручное управление памятью: вы должны явно выделить память перед использованием переменной и явно освободить, когда переменная больше не нужна.
  • Могут иметь фиксированный и произвольный (динамический) размер.
  • Могут иметь произвольное количество элементов.
  • Не имеют ограничений на размер, за исключением естественных.
  • Могут создавать утечки памяти, если вы забудете освободить память.
  • Переменная = указатель. Данные находятся по указателю.
  • Могут быть локальными и глобальными.

Например, к статическим данным относятся:

  • Integer , Char , Extended , Boolean и другие простые типы.
  • Перечислимые типы и тип-диапазон (вообще-то, это тоже простые типы).
  • Статические массивы ( array[0..5] of Integer ).
  • Множества.
  • Процедурный тип
  • Записи
  • Классические объекты Паскаля ( T = object , аналог записей).
  • Файлы («паскалевские» file и textfile ).
  • Паскалевские строки («короткие строки»: ShortString и String[20] ).
  • и т.п.

К динамическим:

  • Строки ( String , AnsiString , WideString , UnicodeString ).
  • Указатели: типизированные ( PInteger , PRecord и др.) и нетипизированные (Pointer).
  • Динамические массивы ( array of Integer ).
  • Объекты ( T = class , аналог указателя на запись).
  • Интерфейсы ( T = interface , аналог указателя на запись с полями из процедурного типа).
  • Варианты ( Variant ).
  • и т.п.

Это не полный список. Я мог кого-то забыть. Кроме того, здесь не указаны составные типы (пользовательские типы данных).

Управляемые и неуправляемые данные

Если вы сравнивали список характеристик динамических переменных со списком динамических типов данных в предыдущем пункте, то могли увидеть небольшую нестыковку:

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

«Гм, когда это я выделял память под строки?»

Хороший вопрос. Дело в том, что динамические типы данных дополнительно делятся ещё на две категории: неуправляемые и управляемые (также иногда называемые авто-финализируемыми, автоматическими, авто-управляемыми, ссылочными или типами с автоматическим управлением временем жизни). Неуправляемые типы данных — это:

  • Указатели
  • Объекты

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

Прочие типы (строки, динамические массивы, интерфейсы, варианты) относятся к управляемым типам. Хотя вы всё ещё должны создавать и удалять их, но если вы этого не сделаете (не удалите), то за вас эту операцию выполнит компилятор. Иными словами, операции освобождения памяти всё ещё есть, но для управляемых типов они скрыты под капотом языка. Автоматические типы «притворяются» простыми статическими типами, хотя на самом деле являются динамическими типами. Это — большой источник подводных камней для новичков, которые пытаются обращаться с этими типами как с простыми статическими — что далеко не всегда возможно (вспомните про «переменная» <> данные). Конечно же, ничто не мешает вам явно освобождать память — и иногда это необходимо делать, хотя и очень редко: например).

Чтобы знать, когда необходимо удалять память переменной, компилятор отслеживает её время жизни: когда переменная уходит из области видимости (к примеру: завершается процедура, чью локальную переменную отслеживает компилятор). Кроме того, почти для всех автоматических типов компилятор использует дополнительный механизм: счётчик ссылок. Суть его заключается в том, что на одни и те же данные может ссылаться несколько переменных. К примеру, если у вас есть строка, а вы присваиваете её второй переменной, то в действительности вы не создаёте две строки — просто для данных строки увеличивается счётчик ссылок. Для каждой добавляемой ссылки (т.е. при присваивании, передаче как параметр в подпрограмму и т.п.) увеличивается счётчик ссылок, а для каждой удаляемой ссылки (т.е. когда переменная выходит из области видимости, при пере-присваивании или присваивании nil) счётчик уменьшается. Данные удаляются, когда счётчик опускается до 0.

Замечание: с определённой точки зрения можно сказать, что статические переменные относятся к управляемым типам данным — ведь память под них вручную вы тоже не распределяете. Однако в этом смысле термин «управляемый тип данных» употребляется крайне редко.

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

Адреса возврата

Но это ещё не всё. Хотя мы полностью разобрали все «наши» данные в нашей программе, остались кой-какие служебные данные, о которых стоит сказать в этой статье.

Вы не задумывались, как происходит вызов функций? Ну, т.е. мы уже поняли, что в адресном пространстве процесса выделен 1 Мб под стек. Мы вызываем процедуру — в стеке создаются её локальные переменные. Это да. Но вопрос: как процессор возвращает управление из процедуры? Ну, с вызовом всё понятно: в коде просто находится инструкция «вызвать код по адресу XYZ». А что с возвратом? Ведь нужно вернуть управление туда, где мы были до вызова — а ведь это не фиксированное место.

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

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

Но вернёмся к нашим баранам. Итак, при каждом вызове функции, кроме её локальных данных, в стеке оказываются и служебные данные, необходимые для возврата управления в точку вызова. Вот как примерно выглядит стек на самом деле:

Весь блок данных, относящийся к одной функции называется стековым кадром или фреймом. Вы можете видеть, как были переданы процедуре аргументы (помните, я говорил, что аргументы — это тоже локальные переменные?), адрес возврата (это как раз и есть указатель на код, на который надо передать управление после завершения работы процедуры — т.е. это наш вызывающий), указатель на предыдущий кадр (это информация, необходимая для восстановления стека обратно после завершения исполнения процедуры). Ну а сама процедура разместила в стеке свои локальные переменные (отличные от аргументов) и временные сохранённые регистры процессора.

Некоторые части, показанные на рисунке, могут отсутствовать в конкретном случае (аргументы, собственные локальные переменные и сброшенные (spilled) регистры), другие являются обязательными (адрес возврата). Точное размещение и порядок этих данных не фиксирован и зависит от модели вызова (также называемой соглашением вызова) — это контракт, соглашение, которому соглашаются следовать вызываемый и вызывающий, чтобы успешно взаимодействовать и понять друг друга. Некоторые общеизвестные модели вызова в Delphi: register , stdcall , cdecl . Очевидно, что если вызывающий будет использовать не то соглашение вызова, что вызываемый — они друг друга не поймут. На том месте, где вызываемый ожидает увидеть (к примеру) свои аргументы будет что-то другое. Это может привести к куче плохих вещей, но иногда это может даже работать.

Ну, хорошо: в стеке хранятся адреса возврата и нам лучше бы не перепутать модель вызова функции при её импорте из DLL. Что ещё?

Видите-ли, проблема тут в том, что данные «разного уровня» (ваши и служебные) хранятся в одном месте (стеке). Смотрите: вот есть режим ядра и режим пользователя. Есть страницы памяти для чтения-записи, а есть для чтения-выполнения. Во всех этих случаях выполняется некий «божественный» контроль. «Божественный» — в том смысле, что происходит он вне «текущего мира», над ним. За программной частью следит аппаратный уровень. Что это значит? Это значит, что если ты ошибёшься, если напортачишь или, даже, специально умыслишь недоброе — ничего страшного не произойдёт. Напакостить тебе никто не даст: между тобой и служебными данными стоит непроницаемая стена аппаратного разграничения. Ты не можешь её обойти на программном уровне, для этого надо подняться на уровень выше. Иными словами, здесь система гарантирует целостность служебных данных.

С другой стороны, служебные и прикладные данные в стеке не разграничиваются вообще никак: они просто идут сплошным потоком. Одно отличается от другого только по «намёкам» в виде того, что «программа знает», что «вот конкретно сейчас ровно на 16 байт от вершины стека лежит адрес возврата». Согласитесь, весьма шаткая грань, верно?

И это действительно так. Если вы будете неаккуратны, то вы можете задеть и испортить служебные данные. Представьте, что будет, если вы случайно запишете в локальную переменную (скажем: статический массив) больше данных, чем его размер — тогда вы запишете данные «за массив» и можете перезаписать при этом адрес возврата. И тогда, когда функция завершит работу и соберётся вернуть управление тому, кто её вызвал, она перейдёт не к вызывающему, а по совершенно мусорному адресу — тому, что вы записали вместо адреса возврата. В этом, кстати, и заключается смысл атак типа «переполнение буфера» (buffer overflow) — злоумышленник ищет в вашей программе ошибку, место, где вы не проверяете размер данных, чтобы подсунуть вам ложный адрес возврата и перейти не к случайному месту, а вполне конкретному — на код злоумышленника.

Фреймы обработчиков исключений

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

И если порча адреса возврата ещё может быть не фатальна: да, вы перейдёте в мусорное место, но вы почти сразу же схлопочите исключение — оно будет поймано обработчиком и вы «откатитесь» к какому-то предыдущему стабильному состоянию. Да, это возможно не всегда, но тем не менее, часто программа может продолжать выполнение после повреждения адреса возврата.

А вот повреждение фрейма обработчика исключения — это всегда фатально. Потому что если испорчен фрейм обработчика исключения, то система даже не сможет передать управления на обработчик (его адрес записан в фрейме)! Как только менеджер исключений увидит, что адреса указывают «в космос» — он немедленно завершит выполнение программы. В этой ситуации вообще ничего нельзя сделать. Программа не сможет даже показать сообщение об ошибке.

Порча памяти

Как вы уже поняли, порча стековой памяти — это может быть очень плохо. Если вы читали мой блог ранее, то можете помнить этот цикл статей по проблемам с памятью. Там я говорил много всего про всевозможные проблемы с памятью — и access violation, и утечки памяти и повреждение памяти (memory corruption), но я достаточно мало сказал там про повреждения стековой памяти. И на это есть простая причина: диагностировать их — обычно очень сложно, если только вам не повезло наткнуться на тривиальный случай (напутали с моделью вызова и т.п.).

Смотрите сами: если данные динамические (выделяются в куче или динамической памяти), то поймать их порчу можно многими способами: мы можем вставить защитные данные до и после блока памяти (и проверять их целостность — уж не вышел ли кто за размер данных переменной и не перезаписал ли данные вне блока памяти), мы можем проверять контрольную сумму данных, мы можем менять атрибуты защиты (и попробуй испорть мои данные под защитой «read only» атрибута! Или даже PAGE_GUARD). В общем, есть куча вещей, просто вагон и маленькая тележка, которые вы можете сделать с динамическими данными. Что ещё более важно: многие проверки могут быть автоматизированы.

Но это совершенно не так для данных на стеке. Фактически, у нас есть 0 (ноль) инструментов для диагностики проблем со стеком!

Надо заметить, что это вовсе не обязательно должно быть так: другие компиляторы имеют (отладочные) опции, позволяющие вставлять аналоги guard-блоков от динамической памяти и в стек. Но вот Delphi такой возможности не имеет.

Ещё надо заметить, что хранение служебных и пользовательских данных в одном месте — это не нерушимое условие. Это так только на архитектуре x86 (x86-32 и x86-64). Другие аппаратные платформы могут реализовывать этот момент по-разному: как похожим, так и совершенно другим способами. К примеру, платформа IA-64 (Intel Itanium), как более молодая платформа, существенно совершеннее технологически. В частности, у IA-64 два стека: один — программный, для пользовательских данных; второй — регистровый, для служебных данных. Таким образом, на этой платформе, хотя переполнение буфера и возможно, но совершенно безопасно для адресов. К сожалению, все эти вкусности нам пока не светят и мы вынуждены работать на этой странной архитектуре x86.

To stack or not to stack?

В связи с этим мне хотелось бы завершить этот цикл кратким сводом правил для новичков: когда следует и когда не следует размещать данные в стеке (читай: делать статическими переменными или делать динамическими переменными):

  • В стеке можно размещать простые типы данных: Integer , Char , записи (если они не включают поля из типов, не рекомендованных к размещению на стеке), перечислимые типы и т.п.
  • Любые массивы (даже статические) надо размещать в динамической памяти.
  • Любые «buffer-like» данные, доступ к которым происходит через низкоуровневые процедуры вроде Move , FillChar и т.п. — надо размещать в динамической памяти.

Замечу, что это рекомендации с точки зрения «защитного программирования». Если вы руководствуетесь другими соображениями, то вы можете использовать и другие правила. Но для новичков, как мне представляется, наибольшую ценность имеет именно подход защитного программирования. Я понимаю, что некоторые правила могут показаться как явный overkill. Да, для опытного программиста это определённо так. Но для начинающего, что лучше: включить отладочный режим в менеджере памяти и найти проблему (в динамической памяти) или полгода искать причину вылета программы из-за повреждения стека?

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

  • Все данные «больших размеров» (близко к размеру страницы и более; т.е. где-то >= 4 Кб) имеет смысл размещать динамически.
  • Листовые функции (т.е. функции которые не вызывают другие) могут размещать в стеке больше данных, чем прочие.
  • Часто вызываемые, высокоуровневые и рекурсивные функции должны стремиться минимизировать размеры данных на стеке.
  • Функции, которые потенциально могут получать управление во время обработки stack overflow (в частности — все обработчики исключений и вызываемые из них функции), должны максимально минимизировать нагрузку на стек.

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

Разумеется, и это не является истиной в последней инстанции. Рассматривайте этот список как предложение, рекомендации.

Последнее, о чём я хотел бы сказать — передача статических данных больших размеров в функции (как правило — массивов):
Вспомните, что аргументы функции — это тоже локальные переменные. Вспомните также, что размер стека ограничен 1 Мб (хотя вы и можете поднять этот предел в опциях проекта). Откуда следует, что передача большого статического массива в процедуру — может быть не самой удачной идеей.

Самое предпочтительное при работе с данными больших размеров — сделать их динамическими. Да, даже если вы работаете с данными фиксированного размера. Всё равно — сделайте их динамическими. Это снизит нагрузку на стек.

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

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

Если вам всё же нужна модификация массива — используйте модификатор var вместо const . Разумеется, в этом случае все изменения, сделанные внутри процедуры, будут видны и вызывающему.

Или. просто используйте динамические массивы!

getmem

Getmem delphi

Автор Kristea Buzila задал вопрос в разделе Другие языки и технологии

Delphi , в чюм разница между «New» И «GetMem» ? и получил лучший ответ

Ответ от Николай[гуру]
На сколько я понял, GetMem позволяет выделить память произвольного размера. Это полезно, когда имеешь дело с WinAPI. Там частенько количество нужной памяти заранее не известно. Её можно узнать используя специальные функции. Затем с помощью GetMem можно выделить строго определённый объём памяти под конкретный тип данных.
New позволяет выделить память под уже известный тип данных. К примеру, если нам нужно выделить память под тип Integer(4 байта) , то следующий код вполне приемлем:
var p:^integer;
.
New(p);
.
Здесь компилятор знает, что нам нужна память именно под тип Integer и выделяет её. Но если мы не указали какой тип данных будет храниться в переменной, то New не сможет выделить нужное количество памяти. К примеру:
var p:Pointer;
.
New(p);
.
Здесь New вернёт nil, поскольку мы не указали под какой тип данных нужно выделить память. В таких случиях лучше использовать GetMem

GetMem — Функция Delphi

function AllocMem(Size: Cardinal): Pointer;
Выделяет в куче блок памяти заданного размера. Каждый байт выделенной памяти выставляется в ноль. Для освобождения памяти используется FreeMem.

var AllocMemCount: Integer;
Содержит количество выделенных блок памяти. Эта переменная увеличивается каждый раз, когда пользователь запрашивает новый блок, и уменьшается, когда блок освобождается. Значения переменной используется для определения количества «оставшихся» блоков.

Тип приложения Доступность AllocMemCount
EXE Приложения, не использующие пакеты и dll-и Delphi могут спокойно обращаться к данной глобальной переменной, т.к. для них существует только один её экземпляр.
EXE с пакетами без dll Приложения, использующие пакеты и не использующие dll-ки также могут спокойно работать с AllocMemCount. В этом случае все модули линкуются динамически, и существует только один экземпляр переменной, т.к. пакеты, в отличие от dll, умеют работать с глобальными переменными.
EXE со статически слинкованными dll Если приложение и используемые им dll-ки являются статически слинкованными с библиотекой выполнения (RTL), AllocMemCount никогда не следует использовать напрямую, т.к. и приложение, и dll-ки будут иметь собственные её экземпляры. Вместо этого следует использовать функцию GetAllocMemCount, живущую в BorlandMM, которая возвращает значение глобальной переменной AllocMemCount, объявленную в BorlandMM. Этот модуль отвечает за распределение памяти для всех модулей, в списке uses который первой указан модуль sharemem. Функция в данной ситуации используется потому, что глобальные переменные, объявленные в одной dll невидимы для другой.
EXE с пакетами и статически слинкованными dll-ками Не рекомендуется создавать смешанные приложения, использующие и пакеты, и статически слинкованные dll-ки. В этом случае следует с осторожностью работать с динамически выделяемой памятью, т.к. каждый модуль будет содержать собственный AllocMemCount , ссылающийся на память, выделенную и освобождённую именно данным модулем.

AllocMemSize

var AllocMemSize: Integer;
Содержит размер памяти, в байтах, всех блоков памяти, выделенных приложением. Фактически эта переменная показывает, сколько байтов памяти в данный момент использует приложение. Поскольку переменная является глобальной, то к ней относится всё, сказанное в отношении AllocMemCount.

GetHeapStatus
function GetHeapStatus: THeapStatus;
Возвращает текущее состояние диспетчера памяти.

TotalAddrSpace Адресное пространство, доступное вашей программе в байтах. Значение этого поля будет расти, по мере того, как увеличивается объём памяти, динамически выделяемый вашей программой.
TotalUncommitted Показывает, сколько байтов из TotalAddrSpace не находятся в swap-файле.
TotalCommitted Показывает, сколько байтов из TotalAddrSpace находятся в swap-файле. Соответственно, TotalCommited + TotalUncommited = TotalAddrSpace
TotalAllocated Сколько всего байтов памяти было динамически выделено вашей программой
TotalFree Сколько памяти (в байтах) доступно для выделения вашей программой. Если программа превышает это значение, и виртуальной памяти для этого достаточно, ОС автоматом увеличит адресное пространство для вашего приложения и соответственно увеличится значения TotalAddrSpace
FreeSmall Доступная, но неиспользуемая память (в байтах), находящаяся в «маленьких» блоках.
FreeBig Доступная, но неиспользуемая память (в байтах), находящаяся в «больших» блоках. Большие блоки могут формироваться из непрерывных последовательностей «маленьких».
Unused Память (в байтах) никогда не выделявшаяся (но доступная) вашей программой. Unused + FreeSmall + FreeBig = TotalFree.
Overhead Сколько памяти (в байтах) необходимо менеджеру кучи, чтобы обслуживать все блоки, динамически выделяемые вашей программой.
HeapErrorCode Внутренний статус кучи

Код Константа Значение
cHeapOk Всё отлично
1 cReleaseErr ОС вернула ошибку при попытке освободить память
2 cDecommitErr ОС вернула ошибку при попытке освободить память, выделенную в swap-файле
3 cBadCommittedList Список блоков, выделенных в swap-файле, выглядит подозрительно
4 cBadFiller1 Хреновый филлер. (Ставлю пиво тому, кто объяснит мне, что это значит). Судя по коду в MEMORY.INC, значения выставляются в функции FillerSizeBeforeGap, которая вызывается при различного рода коммитах (т.е. при сливании выделенной памяти в swap). И если что-то в этих сливаниях не срабатывает, функция взводит один из этих трёх флагов.
5 cBadFiller2 «-/-«
6 cBadFiller3 «-/-«
7 cBadCurAlloc Что-то не так с текущей зоной выделения памяти

8 cCantInit Не вышло инициализироваться
9 cBadUsedBlock Используемый блок памяти нездоров
10 cBadPrevBlock Предыдущий перед используемым блок нездоров
11 cBadNextBlock Следующий после используемого блок нездоров
12 cBadFreeList Хреновый список свободных блоков. Судя по коду, речь идёт о нарушении последовательности свободных блоков в памяти
13 cBadFreeBlock Что-то не так со свободным блоком памяти
14 cBadBalance Список свободных блоков не соответствует действительности

GetMemoryManager

procedure GetMemoryManager(var MemMgr: TMemoryManager);
Возвращает указатель на текущий диспетчер памяти. Структура TMemoryManager описана ниже.
TMemoryManager — структура данных

type
PMemoryManager = ^TMemoryManager;

TMemoryManager = record
GetMem: function(Size: Integer): Pointer;
FreeMem: function(P: Pointer): Integer;
ReallocMem: function(P: Pointer; Size: Integer): Pointer;
end;

var HeapAllocFlags: Word = 2;
Этими флагами руководствуется диспетчер памяти при работе с памятью. Они могут комбинироваться и принимать следующие значения (по умолчанию — GMEM_MOVEABLE):

Флаг Значение
GMEM_FIXED Выделяет фиксированную память. Т.к. ОС не может перемещать блоки памяти, то и нет нужды блокировать память (соответственно, не может комбинироваться с GMEM_MOVEABLE)
GMEM_MOVEABLE Выделяет перемещаемую память. В Win32 блоки не могут быть перемещены, Если они расположены в физической памяти, но могут перемещаться в пределах кучи.
GMEM_ZEROINIT При выделении памяти (например, функцией GetMem) все байты этой памяти будут выставлены в 0. (отличная черта)
GMEM_MODIFY Используется для изменения атрибутов уже выделенного блока памяти
GMEM_DDESHARE Введёны для совместимости с 16-разрядными версиями, но может использоваться для оптимизации DDE операций. Собственно, кроме как для таких операций эти флаги и не должны использоваться
GMEM_SHARE «-/-«
GPTR Предустановленный, соответствует GMEM_FIXED + GMEM_ZEROINIT
GHND Предустановленный, соответствует GMEM_MOVEABLE + GMEM_ZEROINIT

IsMemoryManagerSet

function IsMemoryManagerSet:Boolean;
Возвращает TRUE, если кто-то успел похерить дефолтовый диспетчер памяти и воткнуть вместо него свой.

procedure ReallocMem(var P: Pointer; Size: Integer);
Перевыделяет память, ранее выделенную под P. Реальные действия процедуры зависят от значений P и Size.
P = nil, Size = 0: ничего не делается;
P = nil, Size <> 0: соответствует вызову P := GetMem (Size);
P <> nil, Size = 0: соответствует вызову FreeMem (P, Size) (с тем отличием, что FreeMem не будет обнулять указатель, а здесь он уже равен nil).
P <> nil, Size <> 0: перевыделяет для указателя P память размером Size. Текущие данные никак не затрагиваются, но если размер блока увеличивается, новая порция памяти будет содержать всякий мусор. Если новый блок «не влазит» на своё старое место, он перемещается на новое место в куче и значение P обновляется соответственно. Это важно: после вызова данной процедуры блок P может оказаться в памяти по совсем другому адресу!

procedure SetMemoryManager(const MemMgr: TMemoryManager);
Устанавливает новый диспетчер памяти. Он будет использоваться при выделении и освобождении памяти процедурами GetMem, FreeMem, ReallocMem, New и Dispose, а также при работе конструкторов и деструкторов объектов и работе с динамическими строками и массивами.
SysFreeMem, SysGetMem, SysReallocMem
Используются при написании собственного диспетчера памяти. Другого смысла в них я не нашёл.

var
GetMemCount: Integer;
FreeMemCount: Integer;
ReallocMemCount: Integer;
OldMemMgr: TMemoryManager;

function NewGetMem(Size: Integer): Pointer;
begin
Inc(GetMemCount);
Result := OldMemMgr.GetMem(Size);
end;

function NewFreeMem(P: Pointer): Integer;
begin
Inc(FreeMemCount);
Result := OldMemMgr.FreeMem(P);
end;

function NewReallocMem(P: Pointer; Size: Integer): Pointer;
begin

Inc(ReallocMemCount);
Result := OldMemMgr.ReallocMem(P, Size);
end;

const
NewMemMgr: TMemoryManager = (
GetMem: NewGetMem;
FreeMem: NewFreeMem;
ReallocMem: NewReallocMem);

procedure SetNewMemMgr;
begin
GetMemoryManager(OldMemMgr);
SetMemoryManager(NewMemMgr);
end;

Процедуры работы с динамической памятью

Procedure Dispose(var P: Pointer);

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

После вызова Dispose значение P не определено. При включенной директиве <$I+>, вы можете использовать исключительные ситуации, чтобы обработать эту ошибку.

Procedure FreeMem(var P: Pointer [; Size: Integer]);

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

P — переменная любого типа-указателя, предварительно созданная процедурой GetMem.

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

FreeMem уничтожает переменную P и возвращает память «куче». Если P не указывает на память в «куче», возникает ошибка времени выполнения.

После вызова FreeMem, значение P не определено, и происходит ошибка, если Вы впоследствии ссылаетесь на P^. Вы можете использовать исключительные ситуации, чтобы обработать эту ошибку.

Procedure GetMem(var P: Pointer; Size: Integer);

GetMem создает динамическую переменную определенного размера и помещает адрес блока в переменную Р.

P — переменная любого типа-указателя. Size — выражение, определяющее размер в байтах динамической переменной. Вы должны ссылаться на созданную переменную как P^.

Procedure New(var P: Pointer);

Создает новую динамическую переменную и помещает ее адрес в переменную Р. P — переменная любого типа-указателя. Размер распределенного блока памяти равен размеру типа, на который указывает P. Если памяти недостаточно, чтобы распределить динамическую переменную, возникает исключительная ситуация EOutOfMemory.

При завершении программы, все динамические переменные, созданные процедурами New или GetMem, должны быть уничтожены соответственно процедурами Dispose или FreeMem.

GetMem — Функция Delphi

Delphi. Распределение памяти и динамические библиотеки

Давайте сначала рассмотрим комментарий от «Борланд», создаваемый мастером динамических библиотек DLL.

Важное примечание об управлении памятью в DLL: модуль ShareMem должен быть указан первым в разделе USES вашей библиотеки и в этом же разделе вашего проекта (пункт меню Project -> View Source), если ваша DLL экспортирует любые процедуры или функции, которые передают строки как результат или используют как параметры. Это относится ко всем строкам, передаваемым или получаемым вашей библиотекой — даже тем, которые используются в записях (records) и классах (classes). ShareMem — это интерфейс к библиотеке BORLNDMM.DLL, менеджеру совместного использования памяти, который должен быть развернут наряду с вашим проектом. Чтобы избегать использовать BORLNDMM.DLL, передавайте строки как PChar или ShortString.

Почему эти предосторожности необходимы?
Причина скрывается в способе Delphi выделять и использовать память. В то время как Windows предоставляет родные функции распределения памяти (VirtualAlloc, HeapAlloc, GlobalAlloc, LocalAlloc и т.д.), «Дельфи» осуществляет его собственную политику распределения, или, более точно, существует свой менеджер памяти, который осуществляет ее подраспределение. В языке «Паскаль» («Дельфи») это называют кучей (Heap); C/C++-программисты более знакомы с термином free store. Задача подраспределителя состоит в том, чтобы разместить всю динамическую память: от всей памяти, явно размещенной программистом, до неявно размещенной компилятором при создании строк, динамических массивов и объектов.
Немногие из разработчиков понимают, что они неявно выделяют память в утверждениях типа:

var s: string
.
s: = s + «abc»;

Функции динамического распределения памяти, с которыми большинство пользователей «Дельфи» знакомы, — это GetMem(), FreeMem(), New() и Dispose(). Но фактически многие кажущиеся простыми действия в «Дельфи» приводят к выделению или освобождению памяти кучи. К ним можно отнести: создание объектов с использованием конструктора, строковые переменные и действия над ними, действия над типами данных shortstring, создание и изменение размеров динамических массивов, строковые значения в вариантных переменных, явное распределение памяти функциями GetMem(), FreeMem(), New() и Dispose().
В «Дельфи» все объекты «живут» в куче. Это подобно Java и C#, но не относится к C++, где объекты могут существовать и в стеке, и в куче, и даже в сегменте данных. Разработчики, знакомые с программированием под Windows еще в бытность оной 16-битной, могут задаться вопросом, почему «Дельфи» не использует кучу Windows (через HeapCreate(), HeapAlloc(), HeapFree() и т.д.) или даже через виртуальные функции памяти VirtualAlloc() и VirtualFree(). Причина этого проста — скорость. Функции обслуживания кучи Windows очень медленны по сравнению с родным распределением памяти из «Дельфи». Виртуальное распределение памяти работает еще медленнее, но это только потому, что они не были предназначены для ассигнования большого количества маленьких блоков (для чего, собственно, куча и предназначена). Однако менеджер распределения памяти «Дельфи» в конечном счете все равно вызывает эти виртуальные функции памяти, когда требуются для работы большие блоки памяти, чтобы потом их перераспределять как более мелкие.
Код менеджера памяти находится в модулях System.pas и GetMem.inc, так что компилируется с каждой программой. В общем случае это не является проблемой, но в приложениях, использующих библиотеки, также написанные в «Дельфи», это имеет некоторое значение. Так как DLL — отдельно компилируемое приложение, библиотека получает свою собственную копию менеджера памяти и, таким образом, отдельную кучу. Это — самая важная вещь, которую необходимо помнить: каждое отдельное приложение, будь то исполняемый .exe-файл или динамическая библиотека .dll, управляется его собственным менеджером памяти, обладающим своей кучей. Все последующие проблемы просто возникают из-за наличия одного приложения, .exe либо .dll, которое по ошибке управляет частью памяти, совершенно не принадлежащей его собственной куче.

Что такое «куча»?
Для тех, кто не знаком с понятием кучи и его использованием в «Дельфи»: куча — это область памяти, в которой хранится динамически выделенная приложением память. В наиболее структурированных языках, подобных C и C++, «Дельфи», и даже в новом C# от Microsoft программист может использовать два вида памяти: статический и динамический. Основные типы данных, называемые также типами значений, являются статическими, и их требуемые объемы и конфигурации памяти известны и устанавливаются во время компиляции. Целые числа «Дельфи», перечислительные типы, записи и статические множества — примеры статических переменных. В C все типы данных являются статическими. Поэтому программист должен явно выделить динамическую память через некоторую форму функции распределения памяти подобно malloc(). Размер динамической памяти, с другой стороны, может корректироваться во время выполнения приложения. Пример того — длинные строки «Дельфи», тип class и динамические множества. В Visual Basic’е многие типы данных размещаются динамически, и среди них также присутствуют переменные типа Variant и динамические множества. Как правило, любой тип данных, размер которого может быть изменен во времени выполнения, может динамически размещаться в памяти. С точки зрения компилятора эти два вида памяти являются очень разными и «живут» в совершенно разных секциях памяти приложения: глобальные статически размещенные переменные «живут» в глобальной статической области данных, локальные статически размещенные переменные «живут» в стеке, а динамические блоки памяти «живут» в куче. Фактически такое разделение размещения объектов в памяти заложено в самую основу современного программирования и распространяется до самых недр операционных систем и далее — до самых аппаратных средств ЭВМ непосредственно. Вот почему многие чипы (например, семейство Intel x86) имеют поддержку сегментов стека.
Последняя строка в сгенерированном автоматически комментарии заслуживает отдельного внимания: передавайте строковую информацию, используя PChar- или ShortString-параметры. Это, как кажется, предполагает, что использование PChar или ShortString может решить проблему. Однако это — ошибочное мнение и часто вводит в заблуждение программистов, может убаюкать даже опытных разработчиков и дать им ложное чувство безопасности. Но давайте рассмотрим такой пример:

В DLL:
function GetPChar: PChar;
begin
result: = StrAlloc(13);
StrCopy (result, ‘Привет, Мир!’);
end;

В EXE:
var p: PChar;
.
p := GetPChar;
// чо-то делаем
StrDispose(p); // Вот тут ошибка — возможно, куча DLL повреждена, появляется сообщение «Invalid pointer operation».

И снова причина ошибки — сложившееся восприятие о типе PChar, которое предполагает, что «программный интерфейс приложения (то есть попросту API) Windows делает это таким образом, и значит, это правильно». Но программный интерфейс приложения Windows очень редко размещает PChar для того, чтобы передать его к приложению. От вызывающего приложения требуется выделить для PChar буфер и передать параметр, определяющий его длину, а API тогда сам пишет в этот буфер. Фактически есть очень небольшое, но все-таки весомое преимущество для использования PChar в «Дельфи», так как такой способ передачи строки намного более безопасен и более эффективен. Только очень продвинутые пользователи, имеющие точное и кристально ясное понимание причин так делать, должны их использовать.
При передаче объектов наблюдаем схожую картину:

В DLL:
function GetPChar: PChar;
begin
Result := StrAlloc( 13 );
StrCopy( Result, ‘Привет, Мир!’ );
end;

procedure FreePChar( p: PChar );
begin
StrDispose( p );
end;

В EXE:
var p: PChar;
.
p:= GetPChar;
// что-то делаем
FreePChar( p ); // безвредное для кучи освобождение ресурсов.

В зависимости от того, какое действие совершила исполняемая программа по отношению к объекту, это может причинить ущерб не одной, а сразу обеим кучам. Обратите внимание, что в «Дельфи 6» модуль, освободивший память из кучи другого модуля, фактически может и не испортить структуру кучи. Менеджер кучи держит свободные блоки памяти в связном списке и в моменты удаления блоков пытается сливать два смежных свободных блока. «Invalid pointer operation» происходит тогда, когда модуль пытается освободить последний выделенный блок списка освобождения памяти другого модуля и, будучи не в состоянии распознать некий ошибочный его элемент, пытается слить свободный блок с тем, что, как ему кажется, является маркером (а может быть, и просто мусором), что и приводит к ошибке. Хотя ошибка не выходит за рамки текущего экземпляра, дальнейшая работа менеджера кучи может быть нарушена уже в любом другом месте из-за искажений в структуре кучи.
Вышеупомянутый пример использования PChar может быть «исправлен» следующим образом:

В DLL:
function GetPChar: PChar;
begin
result: = StrAlloc (13);
StrCopy (result, ‘Привет Мир!’);
end;

procedure FreePChar (p: PChar);
begin
StrDispose (p);
end;

В EXE:
var p: PChar;
.
p: = GetPChar;
// сделать кое-что
FreePChar (p); // безвредное для кучи освобождение ресурсов

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

GetMem — Функция Delphi

Язык:
Русский
English

Создает динамическую переменную определенного размера и записывает ее адрес в памяти в указательную переменную.

Объявление

Procedure GetMem(Var P : Pointer; Size : Word);

Режим

Windows, Real, Protected

Замечания

Параметр P — переменная любого указательного типа. Параметр Size — выражение, определяющее размер динамической переменной в байтах. Недавно созданная переменная может быть вызвана как P^.

Если в куче недостаточно свободного пространства для распределения динамической переменной, то происходит ошибка во время выполнения программы.

Ограничения

Самый большой блок, который может быть безопасно распределен в куче равен 65,528 байт (64K-$8).

См. также

Пример

Язык:
Русский
English

Илон Маск рекомендует:  Работа с Cookie. Java Script
Понравилась статья? Поделиться с друзьями:
Кодинг, CSS и SQL