Динамическая память дает возможность реализовать широко применяемую в некоторых программах организацию данных в виде списков. Каждый элемент списка имеет в своем составе указатель на соседний элемент (рис. 6.2), что обеспечивает возможность просмотра и коррекции списка. Если бы в Турбо Паскале не было этого исключения, реализация списков была бы значительно затруднена. В Турбо Паскале можно объявлять указатель и не связывать его при этом с каким-либо конкретным типом данных. Для этого служит стандартный тип POINTER, например: var р: pointer.
1-й элемент списка Указатель — 2-й элемент списка Последний элемент списка NIL Рис. 6.2. Списочная структура данных Указатели такого рода будем называть нешипизированными. Поскольку нети- пизированные указатели не связаны с конкретным типом, с их помощью удобно динамически размещать данные, структура и тип которых меняются в ходе работы программы.
Как уже говорилось, значениями указателей являются адреса переменных в памяти, поэтому следовало бы ожидать, что значение одного указателя можно передавать другому. На самом деле это не совсем так. В Турбо Паскале можно передавать значения только между указателями, связанными с одним и тем же типом данных. Если, например, объявлены переменные pl,p2; "Integer; рЗ : лЯоа1; рр : pointer; то присваивание pl := р2; вполне допустимо, в то время как присваивание pl := рЗ; запрещено, поскольку Р1 и РЗ указывают па разные типы данных. Это огра- ограничение, однако, не распространяется на нетипизированные указатели, по- поэтому мы могли бы записать рр :- рЗ, pl := рр; и тем самым достичь нужного результата.
Читатель вправе задать вопрос, стоило ли вводить ограничения и тут же давать средства для их обхода. Все дело в том, что любое ограничение, с одной стороны, вводится для повышения надежности программ, а с другой — уменьшает мощность языка, делает его менее пригодным для каких-то Применений.
В Турбо Паскале немногочисленные исключения в отношении типов данных придают языку необходимую гибкость, но их использование требует от программиста дополнительных усилий и таким образом свиде- свидетельствует о вполне осознанном действии.
Выделение и освобождение динамической памяти Вся динамическая память в Турбо Паскале рассматривается как сплошной массив байтов, который называется кучей. Физически куча располагается в старших адресах сразу за областью памяти, которую занимает тело программы. Начало кучи хранится в стандартной переменной HeapOrg (рис. 6.3), ко- конец — в переменной HeapEnd. Текущую границу незанятой динамической памяти содержит переменная Heapptr. Памяхь под любую динамически размещаемую переменную выделяется процедурой NEW. Параметром обращения к этой процедуре является типизированный указатель. В результате обращения указатель приобретает значение, соответствующее динамическому адресу, начиная с которого можно разместить данные, например: var i, j : "Integer; г : AReal; begin New(i); end.
После выполнения этого фрагмента указатель 1 приобретет значение, которое перед этим имел указатель кучи HEAPPTR, а сам HEAPPTR увеличит свое значение на 2, т. к. длина внутреннего представления типа INTEGER, с кото- которым связан указатель I, составляет 2 байта (на самом деле это не совсем гак: память под любую переменную выделяется порциями, кратными 8 байтам — см. разд. 6.7). Оператор new (г) ; вызовет еще раз смещение указателя HEAPPTR, но теперь уже на 6 байт, потому что такова длина внутреннего представления типа REAL Аналогичным образом выделяется память и для переменной любого другого типа. После того как указатель приобрел некоторое значение, т. е. стал указывать на конкретный физический байт памяти, по этому адресу можно разместить любое значение соответствующего типа. Для этого сразу за указателем без каких-либо пробелов ставится значок л, например: i = 2, (В область памяти i помещено значение 2} гл = 2*pi; {В область памяти г помещено значение 6.28}\
Расположение кучи в памяти ПК Таким образом, значение, на которое указывает указатель, т. е. собственно данные, размещенные в куче, обозначаются значком Л, который ставится сразу за указателем. Если за указателем нет значка, то имеется в виду ад- адрес, по которому размещены данные. Имеет смысл еще раз задуматься над только что сказанным: значением любого указателя является адрес, а чтобы указать, что речь идет не об адресе, а о тех данных, которые размещены по этому адресу, за указателем ставится Л. Если вы четко уясните себе это, у вас не будет проблем при работе с динамической памятью. Динамически размещенные данные можно использовать в любом месте программы, где это допустимо для констант и переменных соответствую- соответствующего типа, например: гЛ :<* sqr (rA) + \Л - 17; Разумеется, совершенно недопустим оператор г := sqr(rA) + iA - 17; т. к. указателю r нельзя присвоить значение вещественного выражения. Точно так же недопустим оператор гл := sqr (г) ; поскольку значением указателя r является адрес, и его (в отличие от того значения, которое размещено по этому адресу) нельзя возводить в квадрат. Ошибочным будет и такое присваивание: =х; 158 Ядро Турбо Паскаля т. к. вещественным данным, на которые указывает R, нельзя присвоить значение указателя (адрес).
Динамическую память можно не только забирать из кучи, но и возвращать обратно. Для этого используется процедура DISPOSE. Например, операторы disposed) ; dispose(i); вернут в кучу 8 байт, которые ранее были выделены указателям 1 и R (см. выше). Отметим, что процедура dtspose (PTR) не изменяет значения указателя PTR, а лишь возвращает в кучу память, ранее связанную с этим указателем. Од- Однако повторное применение процедуры к свободному указателю приведет к возникновению ошибки периода исполнения. Освободившийся указатель программист может пометить зарезервированным словом NIL. Помечен ли какой-либо указатель или нет, можно проверить следующим образом: const р: 4lGal = NIL; begin if p = NIL then new(p); dispose(p) ; p : NIL; end. Никакие другие операции сравнения над указателями не разрешены. Приведенный выше фрагмент иллюстрирует предпочтительный способ объявления указателя в виде типизированной константы (см. главу 7) с одновременным присвоением ему значения NIL. Следует учесть, что начальное значение указателя (при его объявлении в разделе переменных) может быть произвольным. Использование указателей, которым не присвоено значение процедурой NEW или другим способом, не контролируется системой и может привести к непредсказуемым результатам.
Чередование обращений к процедурам NEW и DISPOSE обычно приводит к "ячеистой" структуре памяти. Дело в том, что все операции с кучей выполняются под управлением особой подпрограммы, которая называется администратором кучи. Она автоматически пристыковывается к вашей программе компоновщиком Турбо Паскаля и ведет учет всех свободных фрагментов в куче. При очередном обращении к процедуре NEW эта подпрограмма отыскивает наименьший свободный фрагмент, в котором еще может разместиться.
Адрес начала найденного фрагмента возвращается в указателе, а сам фрагмент или его часть нужной длины помечается как занятая часть кучи. (Подробнее о работе администратора кучи см. в разд. 6.7). Другая возможность состоит в освобождении целого фрагмента кучи. С этой целью перед началом выделения динамической памяти текущее значение указателя HEAPPTR запоминается в переменной-указателе с помощью процедуры MARK Теперь можно в любой момент освободить фрагмент кучи, на- начиная от того адреса, который запомнила процедура MARK, и до конца дина- динамической памяти.
Для этого используется процедура RELEASE Например: var p,pl ,р?, рЗ,р4,р5 : ЛInteger; begin new(pi); new (p.°); mark(p); new (pi) ; new(p4); new(p5) release (p) f end. В этом примере процедурой MARK{P) в указатель р было помещено текущее значение HEAPPTR, однако память под переменную не резервировалась. Обращение RELEASE (P) освободило динамическую память от помеченного места до конца кучи. На рис. 6.4 проиллюстрирован механизм работы процедур NEW—DISPOSE И NEW—MARK—RELEASE ДЛЯ рассмотренного Примера и для случая, когда вместо оператора RELEASE (Р) используется, например, DISPOSE (РЗ). Следует учесть, что вызов RELEASE уничтожает список свободных фрагментов в куче, созданных до этого процедурой DISPOSE, поэтому совместное использование обоих механизмов освобождения памяти в рамках одной программы не рекомендуется.
Как уже отмечалось, параметром процедуры NEW может быть только типизированный указатель. Для работы с нетипизированными указателями служат процедуры: GETMEM (P, SIZE) — резервирование памяти; О FREEMEM(P, SIZE) освобождение памяти.
Понятно, что наличие нетипизированных указателей в Турбо Паскале (в стандартном Паскале их нет) открывает широкие возможности неявного преобразования типов. К сожалению, трудно обнаруживаемые ошибки в программе, связанные с некорректно используемыми обращениями к про- процедурам NEW и DISPOSE, также могут привести к нежелательному преобразо- преобразованию типов. В самом деле, пусть имеется программа: {i :- HeapOrg; HeapPtr ;-- HeapOrg + 2} {j :- HeapOrg) {HeapPtr ;- HeapOrg} {r := HeapOrg; HeapPtr -•¦» HeapOrg + бу! i ~i * r : " begin new(i); j := i; Y := 2 dispose new(r) ; Integer; Real; r (i); гл := pi; end.
Что будет выведено на экран дисплея? Чтобы ответить на этот вопрос, про- проследим за значениями указателя HEAPPTR.
Перед исполнением программы этот указатель имел значение адреса начала кучи HEAPORG, которое и было передано указателю I, а затем и J. После выполнения DISPOSE (I) указатель кучи вновь приобрел значение HEAPORG, этот адрес передан указателю R в процедуре NEW(R).
После того как по адресу R разместилось вещественное число я = 3,14159, первые 2 байта кучи оказались заняты под часть внутрен- внутреннего представления этого числа. В то же время j все еще сохраняет адрес HEAPORG, поэтому оператор WRITELN(OT) будет рассматривать 2 байта числа л как внутреннее представление целого числа (ведь j — это указатель на тип INTEGER) и выведет 8578. 6.5. Использование указателей Подведем некоторые итоги. Итак, динамическая память составляет 200— 300 Кбайт или больше, ее начало хранится в переменной HEAPORG, а конец соответствует адресу переменной HEAPEND.