Указатель — это всего лишь специальная переменная. В отличие от обычных переменных, которые хранят значение определенного типа, указатель хранит адрес ячейки памяти. Название "указатель" обусловлено тем, что указатель не содержит конкретного значения, а указывает на ячейку памяти, в которой хранится 1 нужное значение.
В Delphi существует два типа указателей: типизированные и нетипизированные (общие). Типизированные указатели можно использовать только с переменными конкретного типа, в то время как нетипизированные указатели могут указывать на любые данные.
Закажи видеокурс по Delphi и получи 106 видеоуроков. Детальное объяснение каждого момента программирования. Кликни на картинку:
ЗАКАЗАТЬ
Для объявления типизированного указателя перед идентификатором типа данных потребуется поместить символ ^:
var
I: Integer;
{ ordinary static variable }
P:^Integer;
{ a typed pointer }
Указатель P может использоваться для указания на любую ячейку памяти, которая содержит целочисленное значение.
Пока не стоит пытаться использовать указатель Р. поскольку он не инициализирован. Когда указатель не инициализирован, он указывает на случайную ячейку памяти. Попытка использования указателя, который указывает на неверную ячейку памяти, подобна попытке прыжка с высоты без парашюта. Вряд ли это можно считать приятным времяпрепровождением.
Чтобы инициализировать указатель, ему следует присвоить адрес ячейки памяти. Для чтения адреса переменной можно использовать операцию @ или функцию Addr:
Р := Addr(I);
Р:=
01
;
Теперь указатель Р указывает на ячейку памяти переменной I. Если требуется выяснить точное местоположения переменной I в памяти, необходимо преобразовать тип указателя в целое число и отобразить его на экране:
Р :=
01
;
WriteLn(Integer(Р));
После того, как указателю присвоен адрес, указатель можно использовать для изменения значения переменной, на которую он указывает. Для получения доступа к переменной, на которую указывает указатель, потребуется разыменовать указатель, записав символ ^после имени указателя, как продемонстрировано в листинге 9.1.
Листинг 9.1. Использование простого типизированного указателя
01.
program
Project1;
02.
{$APPTYPE CONSOLE}
03.
uses
04.
SysUtils;
05.
var
06.
I: Integer;
{ ordinary static variable }
07.
P: ^Integer;
{ a typed pointer }
08.
begin
09.
I :=
101
;
10.
WriteLn(I);
{ 101 }
11.
P := @I;
{ point to I }
12.
P^ :=
202
;
{ change I to 202 }
13.
WriteLn(P^);
{ 202 }
14.
ReadLn;
end
.
Чтобы изменить ячейку, на которую указывает указатель, указателю необходимо присвоить адрес. Однако для изменения значения, хранящегося в ячейке памяти, на которую указывает указатель, потребуется разыменовать указатель. Процедура WriteLn правильно отображает значение, поскольку указатель Р является типизированным и после разыменования в действительности представляет целочисленное значение.
Вторым типом указателя в Delphi является Pointer. Pointer представляет собой тип нетипизированного указателя, который может использоваться для указания на переменную любого типа данных. Чтобы работать со значением, на которое указывает нетипизированньил указатель, вначале нетипизированный указатель нужно привести к другому типу указателя и выполнить его разыменование. Приведение к другому типу указателя не представляет особой сложности, поскольку в Delphi каждый тип данных уже обладает соответствующим типом указателя. Например, указателем на переменную типа Char является PChar, указателем на строку — PString. указателем на целочисленную переменную— PInteger и так далее.
В листинге 9.2 демонстрируется использование типа Pointer и его приведение к другим типам указателей.
Листинг 9.2. Использование нетилизированного указателя
program
Project1;
{$APPTYPE CONSOLE}
uses
SysUtils;
var
I: Integer;
C: Char;
P: Pointer;
{ untyped pointer }
begin
I :=
2004
;
C :=
'd'
;
P := @I;
{ point to I }
{ typecast to an integer pointer, dereference and increment }
Inc(PInteger(P)^);
WriteLn(
'I = '
, PInteger(P)^);
P := @C;
{ point to C }
{ typecast to a char pointer, dereference and convert to 'D' }
PChar(P)^ := Chr(Ord(PChar(P)^) -
32
);
WriteLn(
'C = '
, PChar(P)^);
ReadLn;
end
.
Чтобы использование указателей было полностью безопасным, следует убедиться, что указатель ссылается на существующую ячейку памяти. Для того чтобы указатель ссылался на несуществующую ячейку памяти, можно воспользоваться зарезервированным словом nil. Оно представляет собой постоянное значение, отмечающее несуществующие ячейки памяти. Указатель nil не ссылается ни на одну из ячеек памяти:
program
Project1;
{SAPPTYPE CONSOLE}
uses
SysUtils;
var
I: Integer =
2005
;
P:^Integer @I;
{ автоматически указывает на I }
begin
WriteLn(P^);
{ P указывает на I }
P :=
nil
;
{ P указывает на несуществующую ячейку }
ReadLn;
end
.
Динамические переменные — это переменные, для которых память распределяется в куче (называемой также свободным хранилищем). Куча представляет собой область основной памяти за исключением стека. Размер кучи ограничен только объемом виртуальной памяти компьютера (физическая память плюс файл подкачки).
Резервирование памяти для динамических переменных выполняется с помощью процедуры New. которая принимает единственный параметр указателя. Процедура New резервирует соответствующий объем памяти для хранения значения и присваивает переданному указателю адрес зарезервированного блока памяти. При выделении памяти для указателя указатель не ссылается на другую переменную, а получает собственную область памяти:
var
Р: PInteger;
begin
New(P);
{ создание динамической целочисленной переменной }
end
.
В отличие от статических переменных, управление динамическими переменными не осуществляется автоматически. Это означает, что зарезервированную до них память необходимо освобождать вручную. Для освобождения памяти, зарезервированной процедурой New, применяется процедура Dispose. Если забыть вызвать процедуру Dispose для удаления динамической переменной из памяти, коду может не хватить памяти. Недостаток памяти снижает производительность приложения и может привести к его сбою.
Динамические переменные следует всегда удалять, если они больше не нужны:
var
Р: PInteger;
begin
New(P);
{ создание динамической целочисленной переменной )
{ использование динамической переменной }
Р^ :=
2005
; WriteLn(Р<sup>А</sup>);
Dispose(Р);
{ освобождение памяти }
ReadLn;
end
.
Строки и динамические массивы — это динамические типы, память для которых распределяется в куче, но управляется Delphi автоматически. Для резервирования памяти под динамический массив используется процедураSetLength. Для освобождения памяти, занятой массивом, можно либо вызвать процедуру Finalize, либо присвоить массиву значение nil.
При работе со строковой переменной память резервируется во время присвоения текста переменной. Можно также воспользоваться процедурой SetLength для явного определения длины строковой переменной. Для освобождения памяти, занятой строкой, строковой переменной потребуется присвоить нулевую строку.
var
s:
string
;
begin
s :=
'Dynamic allocation'
;
( освобождение памяти )
s : =
end
.
Для резервирования и работы с нестандартным блоком памяти необходимо использовать процедуры GetMem иFreeMem. Обе процедуры принимают два параметра: указатель, который должен быть связан с зарезервированным блоком памяти, и целое значение, задающее количество резервируемых байт памяти. При вызове процедуры FreeCallпередача второго параметра не обязательна. В случае его передачи передаваемое значение должно соответствовать количеству байтов, зарезервированных процедурой GetCall.
В следующем примере демонстрируется применение процедуры BlockRead для считывания всего файла в динамически зарезервированный блок памяти (результат показан на рис. 9.1).
Листинг 9.3. Загрузка файла в динамически зарезервированный блок памяти
Код загрузки файла в память определен в процедуре ReadFile. Прежде чем резервировать новый блок памяти, процедура должна определить, указывает ли указатель, переданный как параметр Р, на блок памяти. Если да, то необходимо освободить старый блок памяти. В противном случае возникнет серьезная утечка памяти.
if
Р <>
nil
then
FreeMem(Р);
{ освобождение от старых данных }
Процедура GetMem использует функцию FileSize для определения точного объема памяти (в байтах), необходимого для хранения всего файла. После того как память зарезервирована, процедура начинает считывание файла в память.
Рис. 9.1. Приведение типа данных блока памяти в строку
Вероятно, наиболее необычной частью процедуры является локальный указатель Buf ferPos. Прежде чем начать считывание файла в цикле while, процедура присваивает указатель указателю Bu f fer Pos:
BufferPos := P;
Указатель BufferPos указывает на точный адрес памяти, по которому процедура BlockRead должна сохранить 1024байта, считываемые из файла:
BlockRead(Src, BufferPos",
1024
, BytesRead);
После того как процедура BlockRead сохранит 1024 байта в блоке памяти, необходимо обновить указатель BufferPos.Если этого не сделать, процедура BlockRead выполнит считывание всего файла, но при этом перезапишет старые данные последними 1024 байтами из файла.
По завершении считывания 1024 байт процедурой BlockRead указатель BufferPos потребуется переустановить для указания на следующие 1024 байта памяти. Это выполняется путем приведения типа указателя BufferPos к Integer и последующего увеличения адреса указателя на число байт, считанных из файла:
Ine(Integer(BufferPos), BytesRead);
Без использования указателя BufferPos эта процедура не работала бы. Указатель BufferPos позволяет выполнять над параметром-переменной типа указателя действия, которые в противном случае были бы недопустимыми — изменять адрес, на который он указывает. Если бы мы изменяли адрес, указываемый исходным указателем, каждый вызов процедуры BlockRead уменьшал бы объем доступной памяти на 1024 байта, поскольку следующая строка перемещала бы указатель на 1024 байта вглубь блока памяти.
Чтобы отобразить весь текстовый файл, достаточно преобразовать тип данных блока памяти в строку:
WriteLn(
string
(FilePtr));
Указатели можно использовать для доступа к элементам массива. Для считывания адреса первого элемента массива {указания на первый элемент) можно воспользоваться следующим кодом:
var
Numbers:
array
[
1..20
]
of
Integer; PI : PInteger;
begin
PI := ^Numbers;
end
.
Для указания на другой элемент массива достаточно увеличить значение указателя с помощью процедуры Inc.Однако перед его передачей процедуре Ine тип указателя потребуется привести к целочисленному:
Inс(Integer(PI));
{ перемещение к следующему элементу }
Процедура Ine увеличивает значение указателя не на 1, а на SizeOf ( ТипЭлемента). В данном случае она увеличивает значение указателя на 4 (SizeOf (Integer) ), тем самым выполняя перемещение на 4 байта вглубь памяти к адресу следующего элемента массива. Листинг 9.4 демонстрирует доступ к элементам массива с помощью указателя.
Листинг 9.4. Доступ к массиву через указатель
01.
program
Project1;
02.
{$APPTYPE CONSOLE}
03.
uses
04.
SysUtils;
05.
var
06.
Numbers:
array
[
1..20
]
of
Integer;
07.
I: Integer;
08.
PI: PInteger;
09.
begin
10.
{ standard access }
11.
for
I := Low(Numbers)
to
High(Numbers)
do
12.
begin
13.
Numbers := I;
14.
WriteLn(Numbers);
15.
end
;
16.
{ pointer access }
17.
PI := @Numbers;
{ point to first element in Numbers }
18.
for
I := Low(Numbers)
to
High(Numbers)
do
19.
begin
20.
PI^ := I;
21.
WriteLn(PI^);
22.
Inc(Integer(PI));
{ move to next element }
23.
end
;
24.
ReadLn;
end
.