Все переменные, действующие в программе, перечисляются в разделах Var блоков программы. Каждая переменная имеет определенное имя. Транслятор, анализируя разделы Var, формирует специальные таблицы, в которых указывается, сколько памяти в сегменте данных или в сегменте стека должно быть выделено каждой переменной. Реальное выделение памяти для переменной производится при активизации блока, т.е. при запуске программы или при обращении к процедуре или к функции. Память, выделенная переменным при активизации блока, сохраняется за ними на весь период работы блока. Такие переменные называются статическими. Доступ к статической переменной, т.е. чтение или запись ее значения, осуществляется по имени переменной. Здесь имеет место прямая адресация переменной.
В Паскаль-программе имеется также возможность выделять память для переменных в процессе работы программного блока. Такие переменные называются динамическими. Доступ к динамической переменной осуществляется не по имени (ибо имени они не имеют), а по адресу, который называется в этом случае ссылкой, или указателем. Форма доступа в основном аналогична той, которая принята для адресного типа данных. Используемая в этом случае адресация называется косвенной.
Разницу между статическими и динамическими переменными можно проиллюстрировать следующим примером. Предположим, что места в аудитории пронумерованы, как в зрительном зале. Преподаватель может сказать: "Студент Иванов, предъявите домашнее задание". Он может обратиться иначе: "Студент, сидящий на втором месте пятого ряда, выйдите к доске". В первом случае - это обращение по имени (прямая адресация), во втором - обращение по адресу (косвенная адресация).
Динамические переменные наиболее часто используют для реализации связанных структур данных. Отдельные виды таких структур называют стек, очередь, дек, дерево и др.
Проиллюстрируем некоторые особенности связанной структуры на примере очереди к врачу. Каждый пациент занимает произвольное место возле кабинета врача, но знает, за кем он стоит. Другими словами, каждый элемент очереди содержит ссылку на следующий элемент (рис.3).
Пусть один из пациентов покидает очередь. Это не требует перемещения в пространстве остальных пациентов. Просто пациент, следующий после убывшего, запоминает нового человека. В этом случае изменяется значение ссылки у одного элемента очереди (рис.4).
После исключения элемента из очереди соответствующую ячейку памяти (стул возле кабинета) можно освободить и использовать для других целей.
Чтобы вставить новый элемент в очередь, здесь достаточно изменить две ссылки (рис.5). При этом, как и ранее, не требуется перемещения элементов очереди в пространстве.
В программе адрес динамической переменной - это содержимое указателя, который называют также ссылочной переменной. Ссылочная переменная имеет имя, описывается в разделе Varи является, следовательно, статической.
Как уже было отмечено, ссылочная переменная содержит адрес динамической переменной заданного типа. Тип динамической переменной описывается следующим образом:
Type <тип ссылочной переменной> = ^ <тип динамической переменной>
Пример 1.
Type AnsPoint = ^integer;
BerPoint = ^real;
RecType = record
a,b : real;
m,n : integer
end;
RecPoint = ^RecType;
Var P1,P2 : AnsPoint;
Q1,Q2 : BerPoint;
R1,R2 : RecPoint;
Адресные переменные, описанные в предыдущем разделе, и ссылочные переменные во многом совпадают между собой. Общим между ними является то, что каждая из них описана в разделе Var, занимает четыре байта и определяет адрес некоторого поля памяти. Различия:
1) Присваивание конкретных значений адресной переменной производится оператором @, функцией Addr или процедурой Ptr. Оператор @ и функция Addr присваивают этой переменной адрес уже известной, т.е. объявленной ранее в разделе Var переменной. Процедура Ptr формирует адрес поля памяти по заданным значениям сегмента и смещения. Ссылочным переменным конкретное значение адреса, как это будет показано ниже, присваивается с помощью процедуры New.
2) Для адресных переменных атрибуты адресуемых полей памяти на этапе трансляции неизвестны. Поэтому для обработки адресуемого поля используется аппарат приведения типов. Ссылочные переменные всегда задают адрес поля памяти уже известного типа. Поэтому обработка адресуемого поля может выполняться без приведения типов. Например, для примера 1 можно написать
P1^:=34; Q1^:=-18.55;
R1^.a:=5.8; R1^.b:=-13.3; R1^.m:=6; R1^.n:=16.
Как адресные, так и ссылочные переменные часто называют указателями. Если по тексту требуется подчеркнуть различие между двумя видами таких переменных, в случае необходимости ссылочные переменные будем называть типизированными указателями.
Рассмотрим формирование указателей для связанных структур данных.
Динамическая переменная, используемая для реализации связанных структур, имеет тип записи. Эта запись содержит две части: информационную (например, числа) и ссылку на другую динамическую переменную такого же типа.
Предположим, что тип указателя мы обозначили именем PoinType, а тип динамической переменной - именем DynType. Пусть информационная часть динамической переменной - это одно число типа integer. Тогда описание типов ссылочной и динамической переменных может иметь следующий вид:
TypePoinType = ^DynType;
DynType = record
Inf : integer;
Next : PoinType;
end;
Var L,R,P : PoinType;
Имена L, R, P - это имена переменных типа PoinType, т.е. в данном случае имена указателей. Эти имена описаны в разделе Var; следовательно, L, R, P - статические переменные. При активизации программного блока каждой из этих переменных выделяется поле памяти длиной 4 байта, но содержимое этих полей остается неопределенным.
В Паскаль-программе любое имя может быть использовано только после его описания. Однако в приведенном выше описании типа это правило нарушено. В самом деле, транслируя фразу «PoinType = ^DynType», компилятор встречает имя DynType, которое еще не было описано. Если переставить местами описание указателя PoinType и описание записи DynType, то такая же ситуация возникнет по отношению к имени PoinType.
По поводу упомянутого выше правила в Паскале имеется еинственное исключение: описание типа указателя, в котором используется тип динамической переменной, должно предшествовать описанию типа динамической переменной.
Резервирование поля памяти для динамической переменной и засылка адреса этого поля в ссылочную переменную выполняется предописанной процедурой New(L), где L - имя ссылочной переменной (имя указателя). При этом выделяется столько байтов памяти, сколько требует тип динамической переменной, с которой связан указатель L.
При запуске Паскаль-программы формируется специальная таблица, содержащая сведения об участках свободной памяти. При срабатывании процедуры New из этой таблицы выбирается нужный участок, адрес которого присваивается соответствующему указателю. Информация о выбранном участке вычеркивается из таблицы свободной памяти.
Пусть в памяти ЭВМ размещены два списка динамических переменных (рис.6).
Рассмотрим действие различных операторов на состояние этих списков.
a) L := R (рис.7).
б) L^ := R^ (рис.8).
в) L^.Inf := R^.Inf (рис.9).
г) L^.Next := R^.Next (рис.10).
д) L := nil (рис.11).
При изменении указателей в первоначально связанных структурах могут оставаться такие элементы, доступ к которым уже невозможен, так как их адрес потерян при изменении значений соответствующих указателей (а: элементы 30,40; б: элемент 40; г: элемент 40; д: элементы 30,40). Но эти переменные продолжают занимать память, хотя они уже и не нужны. Неиспользуемые динамические переменные образуют "мусор", который загромождает память, и при своем накоплении может сделать невозможным дальнейшее функционирование программы.
Для предотвращения накопления мусора необходимо сведения о динамических переменных, которые становятся ненужными, возвращать в таблицу свободной памяти. Эта работа выполняется предописанной процедурой Dispose(L), которая освобождает память, затребованную процедурой New(L) для переменной L^, передавая об этом сведения в таблицу свободной памяти. Содержимое поля L^ при этом не изменяется, но может быть использовано для размещения другой динамической переменной.
Обычная схема использования динамической переменной имеет вид:
New(L);
........
Обработка L^
........
Dispose(L);
L:=nil;
Процедура Dispose не изменяет указатель L. Поэтому после освобождения памяти, адресуемой указателем L, ему целесообразно присвоить пустое значение nil.
Для выделения динамической памяти может использоваться также процедура
GetMem(P:pointer; Size:word),
которая создает новую динамическую переменную P^ размером Size. В этом случае освобождение динамической памяти должно выполняться процедурой
FreeMem(P:pointer; Size:word).
Отличия между процедурами New, Dispose и GetMem, FreeMem:
- в процедурах GetMem, FreeMem указатель P может быть любого типа, в процедурах New, Dispose это может быть только типизированный указатель;
- процедура GetMem (FreeMem) может выделять (удалять) любой заданный объем памяти (но не более 64 Кбайта), процедура New (Dispose) - только тот объем памяти, который определен описанием типизированного указателя.
Пример 2.
Type PoinType = ^DynType;
DynType = record
Inf : integer;
Next : PoinType;
end;
Var P : PoinType;
Begin
...............
New(P);
...............
Dispose(P);
...............
GetMem(P,SizeOf(DynType));
..........................
FreeMem(P,SizeOf(DynType));
..........................
Здесь процедуры New, Dispose и GetMem, FreeMem действуют совершенно одинаково.
Типизированному указателю, как и нетипизированному, можно присвоить адрес конкретного поля памяти.
Пример 3.
Type ByteAr = array[1..2] of byte;
ByteArPtr : ^ByteAr;
Var k : integer;
p : ByteArPtr;
Begin
p:= @k;
Следовательно, динамическая переменная p^ и статическая переменная k определяют одно и то же поле памяти. Используя в программе имя k, мы можем обрабатывать это поле как переменную типа integer; при использовании имени p^ это же поле рассматривается как массив из двух элементов типа byte. Нетрудно заметить сходство примененной здесь методики с аппаратом абсолютных переменных.