какие они
бывают, что с ними можно делать,
где брать память, чтобы им было
куда указывать и как надежно
программировать ДРП
Указатели
Указателем называется переменная,
содержащая в себе некоторый адрес (имеется
в виду адрес в оперативной памяти). Как
правило указатели содержат адрес
некоторой другой переменной или
выделенной динамически области памяти (говорят:
указывают или ссылаются на эту
переменную или область памяти).
Var
P: Pointer; {объявляем переменную
-указатель}
S: Byte; {и некую переменную}
Begin
{ . . . }
P:= @S; {теперь P указывает на S}
{ . . . } End.
В данном примере указатель
объявляется и инициализируется
значением адреса переменной S (@S).
Обращения (чтение или запись значения) к
указателю и переменной в результате
есть обращения к различным областям
памяти; однако обращение по
указателю (P^) есть обращение к
области памяти, в которой расположена
переменная S.
Естественно, что сам указатель не
может содержать информации о типе
(то есть структуре) того, что по этому
адресу расположено. Поэтому при
обращении по указателю необходимо
подсказать компилятору как
использовать то, на что ссылается
указатель, что можно делать с помощью
приведения типов, что не всегда удобно.
Поэтому в введены так называемые типизированные
указатели. Физически типизированный
указатель ничем не отличается от
нетипизированного (типа Pointer),
однако компилятор рассматривает
область памяти по адресу указателя как
переменную некоторого определенного
типа и не испытывает затруднений при
выборе способа ее обработки. Для того,
чтобы объявить тип "типизированный
указатель" ("указатель на тип")
используют знак ^ :
Type
<имя_типа_указателя> = ^<тип>;
Для того, чтобы обратиться по
указателю к области памяти, на которую
указывает указатель используют тот же
знак, но справа от переменной типа
указатель:
<указатель>^
Данная структура рассматривается как
переменная. Если <указатель> есть
указатель типизированный, то тип этой
переменной известен компилятору, в
противном случае, как уже отмечалось,
необходимо приведение типа данной
переменной (ее можно привести к любому
типу, так как размер ее не определен).
Операции над указателями,
или укажем кому-нибудь на что-нибудь
Естественно, что само наличие любого
типа и даже объектов (экземпляров,
переменных) данного типа, вполне
бессмысленно до тех пор, пока нет
возможности присваивать им (объектам)
значения и применять операции.
Присвоение значений указателям
производится
оператором присваивания
непосредственного адреса переменной,
процедуры или функции:
<указатель>:= @<идентификатор>;
в <указатель> записывается
адрес переменной, типизированной
константы, процедуры или функции (операция
@ так и называется - "взятие
указателя") или
<указатель1>:= <указатель1>;
оператором присваивания адресного
выражения:
<указатель>:= <адресное выражение>;
где <адресное выражение>
есть выражение, результатом
вычисления которого является адрес (в
форме указателя) - указатель, функция,
возвращающая указатель etc.
в результате выполнения операций
выделения памяти соответствующими
процедурами и функциями.
Естественно, что указатели (в реальном
режиме IBM PC) могут принимать значения от 0000:0000
до FFFF:FFFF, то есть могут обеспечить
доступ к любой ячейке
оперативной памяти (в пределах первого
мегабайта).
Особое место во множестве значений
указателей занимает "пустой адрес"
- Nil. Он определен всегда
и, соответственно, с ним можно сравнить
любой указатель. Считается, что если
указатель равен Nil, то
переменная, на которую он указывает
отсутствует в ОП (соответственно, ее
нельзя оттуда удалить).
Значения указателей можно
инкрементировать и декрементировать,
используя, соответственно, Inc и Dec,
причем для типизированных указателей
производится увеличение или уменьшение
содержимого указателя на размер
соответствующего ему типа. На практике
это означает следующее. Допустим в
некоторой области памяти (последовательно)
расположены несколько однотипных
переменных, и есть указатель,
ссылающийся на первую из них. Тогда
увеличение указателя с помощью Inc
приведет к тому, что он будет ссылаться
на следующую переменную.
Над указателями также определены
операции сравнения на равенство (=)
и неравенство (<>), других
сравнений не допускается.
И, как говорилось выше, над указателями
определена операция "обращение по",
результатом которой является область
памяти (с типом или без), на которую
ссылается указатель ^.
Как правило нельзя предвидеть
количество переменных, которое
понадобится при решении той или иной
задачи. Кроме того, одна и та же задача
при различных исходных данных может
требовать различное количество
переменных. Предусматривать в программе
место для максимально возможного
количества переменных, как правило,
нерационально, причем по двум причинам. Первая
- возможности программы изначально
ограничиваются программистом, а не
возможностями машины, вторая -
размер программы неоправданно
раздувается, потому как исходные данные,
требующие максимальных размерностей
достаточно редки.
Вследствие вышеназванных причин
используют так называемое динамическое
распределение памяти (ДРП),
позволяющее выделять (и освобождать)
необходимые участки памяти в процессе
выполнения программы. Естественно, что
процессы выделения и освобождения
жестко связаны с системой адресации и
структурой памяти операционной системы
и ЦВМ. Для того, чтобы не утруждать
программиста высокого уровня
подробностями выделения и управления
памятью, широко используют указатели.
При использовании динамической памяти
всегда следует понимать, что вы хотите
взять - сам указатель или переменную, на
которую он указывает. И поступать
соответственно.
Все динамические переменные
размещаются в так называемой куче (Heap)
- области памяти, не занятой ни кодом, ни
данными, ни системой, предоставляемой
программе при загрузке операционной
системой - посредством менеджера кучи (Heap
Manager). Размеры кучи, предоставляемой
программе операционной системой
задаются директивой компилятора
Минимальный размер
вычисляется программистом исходя из его
личных соображений (программа сбоит при
меньшем, суммарный объем требуемой
памяти равен тому-то, etc.), как, впрочем, и
максимальный. При их задании следует
учитывать, что слишком маленький
минимальный размер, да еще при не совсем
правильной работе приводит к Run-time error
202 - Heap allocation error, а слишком большой
максимальный размер не позволит
вызывать внешние исполняемые модули.
Доступ к динамически размещенным
переменным производится с помощью
указателей. Для работы с указателями
существует несколько процедур и функций,
предоставляемых для самостоятельного
изучения: Addr, CSeg, DSeg, Ofs, Ptr, Seg, SPtr, SSeg.
Pascal содержит две процедуры для
выделения динамических переменных:
Procedure New(Var
P: Pointer); Function New(Pointer_Type, [Consrtuctor_Call]):
Pointer;
создает динамическую
переменную и возвращает указатель
на нее
Procedure GetMem(Var P:
Pointer; Size: Word);
создает динамическую переменную
указанного размера и возвращает
указатель на нее
Размер переменной, создаваемой
процедурой (функцией) New
определяется типом указателя:
выделяется ровно столько памяти,
сколько занимает тип, на который он (указатель)
ссылается. Если указатель является
нетипизированным, ни создания
переменной, ни изменения значения
указателя не производится.
Соответственно, существует пара
функций для удаления динамически
размещенных переменных из памяти.
Procedure Dispose(Var
P: Pointer);
удаляет переменную,
созданную с помощью New
Procedure FreeMem(Var
P: Pointer; Size: Word);
удалаяет переменную, размещенную
с помощью GetMem
Переменные, размещенные посредством New,
следует удалять только посредством Dispose.
То же самое относится и к паре GetMem
- FreeMem.
При использовании New возможно
запоминание текущего состояния кучи (Heap)
с целью последующего возвращения кучи в
исходное состояние. При этом
используются
procedure Mark(var
p: pointer);
запоминает в указателе
текущее состояние кучи (HeapPointer)
procedure Release(var
p: pointer);
восстанавливает состояние кучи
из указателя
Таким образом возможно не
использовать операторы Dispose. К
чему приведет использование такого
метода при размещении объектов сказать
трудно, возможно, что все сработает
корректно. Наиболее вероятное
применение последней пары операторов -
при написании собственных обработчиков
ошибок, к примеру HeapFunc.
Надежность,
или как писАть, чтобы программы не
рушились
При использовании динамической памяти
следует предусматривать хотя бы
большинство возможных ошибок, как то:
отсутствие подходящего участка памяти в
куче, попытка повторного удаления
удаленной динамической переменной.
Получить информацию о размерах
свободной памяти можно используя
функции
Function MemAvail: LongInt;
возвращает суммарный объем
свободной кучи
Function MaxAvail: LongInt;
возвращает размер максимального
непрерывного свободного блока в
куче
Существование двух различных функций
объясняется возможной фрагментацией
свободных участков в куче, что
происходит при хаотичном выделении и
удалении динамических переменных.
Кстати, при проверке наличия
доступного места в куче при работе в
защищенном режиме следует учитывать,
что выделение производится в два этапа:
выделение локальной кучи размером в HeapBlock,
а затем распределение пространства по
запросам. Как только текущая локальная
куча исчерпывается, производится
выделение новой.
Для того, чтобы избежать ошибок
повторного удаления автор рекомендует
принять к исполнению следующее правило:
указатель, который не указывает ни на
что (то есть не использовался при
создании переменных или переменная, на
которую он указывал уже удалена) должен
иметь значение Nil; перед каждым
удалением любой динамической
переменной необходимо производить
анализ равенства указателя Nil,
в случае равенства - не производить
удаления:
If P<>NilThen
Begin
Dispose(P) {FreeMem(P, Size);}
P:=Nil; {обязательно!} End;