Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

delphi / 07.адреса и указатели

.doc
Скачиваний:
32
Добавлен:
26.03.2016
Размер:
50.18 Кб
Скачать

ЛЕКЦИЯ 7

Адреса и указатели.

Статически выделяемая память

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

Такое выделение памяти используется всякий раз при объявлении "обычных" переменных в разделе var. Каждая переменная обладает двумя атрибутами: именем и описанием.

var a: integer;

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

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

Адреса

Имя переменной является ее своеобразным (буквенным) адресом. Однако у любой переменной есть также и обычный (цифровой или физический) адрес: номер ячейки, выделенной под эту переменную.

При страничной организации памяти адреса являются составными и состоят из номера сегмента памяти и смещения ячейки относительно начала этого сегмента.

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

Для обращения к статически заданной переменной можно использовать как ее имя, объявленное в разделе var, так и ее физический адрес.

Например, "адрес" одной и той же географической точки можно записать по-разному: "49°47' северной широты и 86°36' восточной долготы" или просто "вершина пика Белуха Восточная".

Указатели

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

Описание указателей

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

var <имя_указателя>: ^<тип_адресуемой_переменной>;

Например:

var p: ^integer;

q: ^real;

s: ^array[1..10] of byte;

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

var <имя_указателя>: pointer;

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

Определение адреса

Физический адрес любой переменной можно узнать при помощи стандартной функции addr(<имя_переменной>):<указатель> или унарной операции @<имя_переменной>.

В зависимости от значения директивы компилятора {$T}, результатом операции @ будет либо типизированный указатель (если установлено {$T+}), тип которого будет определен в соответствии с типом использованной переменной, либо нетипизированный указатель pointer (если установлено {$T-}).

Результат функции addr() совместим с указателями любых типов:

p:= addr(x); {x: real; p: ^byte)

Разыменование

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

<имя_указателя>^

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

Из-за вольностей, допускаемых процедурой addr(), при разыменовании порой могут возникнуть забавные ситуации. Например, в результате выполнения такой вот программы:

const a: array[1..3] of char ='ААА'; {код(А)=128 или 01000000}

var p: ^word;

begin p:= addr(a);

writeln(p^)

end

на экран будет выведено 32896, что в двоичной системе счисления выглядит как 01000000.01000000 (точкой помечена граница двух байтов). Иными словами, коды двух первых букв оказались слитыми в значение типа word.

Замечание: Операции @ и ^ являются взаимно обратными, то есть для любой переменной a и для любого типизированного указателя p верны следующие равенства:

@(p^)= p и (@a)^ = a

Присваивания

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

p:= q; {p:= ^integer; q: ^byte}

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

{p:= ^integer; q: ^byte; t: pointer}

t:= q;

p:= t;

У указателей также существует свой "ноль", который означает, что указатель не указывает никуда:

p:= nil;

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

Сравнения

Для указателей определены две операции сравнения: = и <>.

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

Для разнотипных указателей сравнения невозможны: попытка записать

if p = q then writeln('yes'); {p: ^byte; q: ^integer}

вызовет ошибку уже на этапе компиляции.

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

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

Поскольку к любой переменной можно обратиться двояко - по имени и по адресу, - есть возможность сократить эту избыточность и оставить только один способ. Как мы уже видели, наличие имени означает и наличие адреса. Иными словами, если вы дали переменной имя в разделе var, то в процессе компиляции у нее появится и адрес.

Итак, пусть у некоторой переменной нет имени. Тем не менее можно расположить ее в памяти, выделив под нее необходимое количество байтов, и т.д. У переменной будет адрес, будет значение, но не будет имени. Следовательно, обратиться к такой переменной можно будет только с помощью указателя.

"Безымянные" переменные отличаются от "нормальных" переменных:

Нет имени - нечего описывать в разделе var.

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

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

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

Типизированные указатели

Для выделения памяти служит стандартная процедура new():

new(<имя_указателя>);

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

Например, если переменная p была описана как указатель на integer-переменную, то процедура new(p) выделит два байта; под real-переменную необходимо выделить четыре байта и т.д.

Нетипизированные указатели

Для того чтобы выделить память, на которую будет указывать нетипизрованный указатель pointer, нужно воспользоваться стандартной процедурой getmem(p: pointer; size: word), которая выделит столько байт свободной памяти, сколько указано в переменной size.

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

Типизированные указатели

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

dispose(<имя_типизир_указателя>).

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

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

dispose(p);

p:= nil;

Нетипизированные указатели

Для того чтобы освободить память, на которую указывает нетипизрованный указатель, нужно воспользоваться стандартной процедурой freemem(p: pointer; size: word), которая освободит в памяти столько байтов (начиная с указанного в переменной p адреса), сколько задано в переменной size.