Выделение и освобождение динамической памяти
В приведенном выше примере процедурой MARK(P) в указатель Р было помещено текущее значение HEAPPTR, однако память под переменную не резервировалась. Обращение RELEASE(P) освободило динамическую память от помеченного места до конца кучи. Рис. 6.4 иллюстрирует механизм работы процедур NEW-DISPOSE и NEW-MARK-RELEASE для рассмотренного примера и для случая, когда вместо оператора RELEASE(P) используется, например, DISPOSE(P3).
Следует учесть, что вызов RELEASE уничтожает список свободных фрагментов в куче, созданных до этого процедурой DISPOSE, поэтому совместное использование обоих механизмов освобождения памяти в рамках одной программы не рекомендуется.
Как уже отмечалось, параметром процедуры NEW может быть только типизированный указатель. Для работы с нетипизированными указателями используются процедуры:
GETMEM (P, SIZE) – резервирование памяти; FREEMEM(P, SIZE) – освобождение памяти.
Здесь:
- Р – нетипизированный указатель;
- SIZE – размер в байтах требуемой или освобождаемой части кучи.
Рис. 6.4. Состояние динамической памяти: а) перед освобождением; б) после Dispose(p3); в) после Release(p)
За одно обращение к куче процедурой GETMEM можно зарезервировать до 65521 байта динамической памяти.
Использование процедур GETMEM-FREEMEM, как и вообще вся работа с динамической памятью, требует особой осторожности и тщательного соблюдения простого правила: освобождать нужно ровно столько памяти, сколько ее было зарезервировано, и именно с того адреса, с которого она была зарезервирована.
Нетрудно обнаружить, что наличие нетипизированных указателей в Турбо Паскале (в стандартном Паскале их нет) открывает широкие возможности неявного преобразования типов. К сожалению, трудно обнаруживаемые ошибки в программе, связанные с некорректно используемыми обращениями к процедурам NEW и DISPOSE, также могут привести к нежелательному преобразованию типов. В самом деле, пусть имеется программа:
var i,j: Integer; r: Real; begin new(i); {i: = HeapOrg; HeapPtr: = HeapOrg + 2} j: = i; {j: = HeapOrg} j: = 2; dispose(i); {HeapPtr: = HeapOrg} new(r); {r: = HeapOrg; HeapPtr: = HeapOrg + 6} r: = pi; WriteLn(j) end.
Что будет выведено на экран дисплея? Чтобы ответить на этот вопрос, проследим за значениями указателя HEAPPTR. Перед исполнением программы этот указатель имел значение адреса начала кучи HEAPORG, которое и было передано указателю I, азатем и J. После выполнения DISPOSE(I) указатель кучи вновь приобрел значение HEAPORG, этот адрес передан указателю R в процедуре NEW(R). После того как по адресу R разместилось вещественное число pi=3.14159, первые 2 байта кучи оказались заняты под часть внутреннего представления этого числа. В то же время J все еще сохраняет адрес HEAPORG, поэтому оператор WRITELN(J^) будет рассматривать 2 байта числа pi как внутреннее представление целого числа (ведь J – это указатель на тип INTEGER) и выведет 8578.