Указатель - это ссылка на данные или код вашей программы. Он  представляет адрес в памяти элемента,  на который указывает.  Использование указателей позволяет писать большие  и  более гибкие  программы и  особенно полезно,  когда вы начинаете писать объектно-ориентированные программы.

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

* Зачем и когда используются указатели.

* Что такое указатель.

* Как использовать указатели.

* Эффективная работа с указателями.

Рано или поздно каждый программист,  работающий на  Паскале,  попадает в ситуацию, требующую использования указателей. Указатели требуется применять в следующих случаях:

* Если  ваша  программа  работает с большими объемами данных (общий объем которых превышает 64К).

* Если  ваша программа во время компиляция использует данные   неизвестного размера.

* Если программа использует временные буферы данных.

* Если ваша программа работает с несколькими типами данных.

* Если ваша программа использует связанные списки данных или   объектов.

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

По мере того как программы становятся более сложными, и требуются  работа  с  большим количеством данных,  область объемом в  64К, зарезервированная в Borland Pascal для данных,  может  оказаться  недостаточной,  чтобы содержать все необходимые программе  данные. Указатели позволяют вам обойти эту проблему.

Когда вы  описываете в Borland Pascal глобальные переменные,  компилятор выделяет для них память в области,  которая называется  сегментом данных.  Сегмент данных имеет максимальный размер 64К.  Это означает, что общий объем всех ваших глобальных переменных не  может превышать 64К.  Для многих программ этот предел значения не  имеет, но в некоторых случаях  вам может  потребоваться  больший  объем.

Примечание: Локальные  переменные не помещаются в сегмент данных и в пределе 64К не учитываются.

Предположим, например,  что у вас есть программа,  требующая  массива в  400  строк  по 100 символов каждая.  Для этого массива  требуется примерно 40К,  что меньше максимума в 64К. Если остальные ваши  переменные  помещаются в оставшиеся 24К,  массив такого  объема проблемы не представляет.

Но что если вам нужно два таких массива?  Это потребовало бы  80К, и 64К сегмента данных не хватит. Чтобы работать с большими  объемами данных,  вам нужно использовать динамически распределяемую область памяти.  Ваша программа может выделить в  динамически  распределяемой  области 80К,  поддерживая указатель в виде ссылку  на адрес данных.  Указатель занимает в сегменте данных  только 4  килобайта.

Что такое динамически распределяемая область памяти?

Динамически распределяемая область памяти - это вся  память,  которую ваша  операционная система делает доступной для программы  и которая не используется ее кодом,  сегментом данных  и стеком.  Объемом распределяемой  динамической памяти вы можете управлять с  помощью директивы компилятора $M.

Обычно в Borland Pascal вы можете зарезервировать  память  в  динамически распределяемой  области, получить к ней доступ через  указатель, а затем снова освободить память. Подробности о распределении памяти  в динамически  распределяемой  области вы можете  найти ниже в разделе "Как использовать указатели?".

Некоторые элементы данных Borland Pascal (в частности, строки и массивы) требуют задания размеров во время компиляции,  даже  если при выполнении программы вам не потребуется  вся  выделенная  память. Простым примером может быть программа, считывающая вводимую пользователем строку, например, имя пользователь. Чтобы записать имя в обычной строковой переменной, вам потребовалось бы зарезервировать достаточно памяти для максимальной возможной  строки, даже если набранное имя содержит всего несколько букв.  Если  вы распределяете переменные в динамически распределяемой  области  памяти  во  время  выполнения, то  можете выделить точно столько  байт, сколько необходимо для фактической строки данных.

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

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

 

Другим общим примером использования временной памяти является сортировка.  Обычно когда вы сортируете большой объем данных,  то делаете копию массива,  сортируете копию,  а затем записываете  отсортированные данные обратно в исходный массив.  Это  сохраняет  целостность ваших данных,  но требует также наличия во время сортировки двух копий данных.  Если вы хотите распределить сортируемый массив в динамически распределяемой памяти,  то можете отсортировать его и скопировать обратно в оригинал, а затем уничтожить  сортируемый массив, освободив память для других нужд.

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

Примечание: О нетипизированных параметрах-переменных  рассказывается в Главе 9 ("Процедуры и функции") "Руководства по языку".

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

Указатель - это какой-либо адрес в памяти вашего компьютера.  Это может быть адрес переменной,  записи данных,  либо  процедуры  или функции.  Обычно вам не важно, где расположен элемент в памяти. Вы можете просто ссылаться на него по имени, и Borland Pascal  знает, где его нужно искать.

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

var SomeNumber: Integer;

Вам не нужно беспокоиться о том,  где SomeNumber находится в  памяти. Именно для этого задается имя.

Адрес размещения  SomeNumber в памяти можно найти с помощью  операции @.  @SomeNumber - это адрес вашей целочисленной переменной. Вы можете присвоить этот адрес переменной-указателю, то есть  переменной, содержащей адрес данных или кода в памяти.

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

Таким образом,  чтобы использовать тот же пример SomeNumber,  вы можете присвоить его адрес переменной-указателю:

var

   SomeNumber: Integer;

   SomeAddress: Pointer;

begin

   SomeNumber := 17; {присвоить SomeNumber значение}

   SomeAddress := @SomeNumber;  {присвоить SomeAddress адрес}

   SomeAddress := Addr(SomeNumber);  {другой способ получения

 адреса}

end.

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

Обычно вы  определяете ссылочные типы,  которые указывают на  конкретный вид элемента, например, целое значение или запись данных. Как  вы  далее увидите,  можно извлечь преимущество из того  факта, что указателю известно, на что он указывает. Чтобы определить типизированный указатель, вы можете описать новый тип, определенный символом каре (^),  за которым следуют  один  или  более  идентификаторов. Например, чтобы определить указатель на Integer,  вы можете сделать следующее:

type PIneger = ^Integer;

Теперь вы можете описать переменные типа PInteger.  Если  вы  не собираетесь часто использовать ссылочный тип, то можете  просто описать переменные,  как указатели на  уже  определенный  тип.  Например,  если вы определили PInteger как ^Integer, то следующие  описания переменной эквивалентны:

var

   X: ^Integer:

   Y: PInteger;

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

Ниже показаны некоторые примеры разыменования указателя:

type PInteger = ^Integer;

 

var

   SomeNumber: Integer;  { присвоить SomeNumber 17 }

   SomeAddress := @SomeNumber;  { SomeAddress указывает на

SomeNumber }

   Writeln(SomeNumber);   { напечатать 17 }

   Writeln(SomeAddress); { не допускается; указатели печатать

   нельзя }

   Writeln(SomeAddress^); { напечатать 17 }

   AnotherAddress := SomeAddress;  { также указывает на

SomeNumber }

   AnotehrAddress^ := 99;   { новое значение для SomeNumber }

   Writeln(SomeNumber);   { напечатать 99 }

end.

Пример 8.1 Простые примеры разыменования указателей.

Наиболее важными строками в Примере 8.1 являются следующие:

   AnotherAddress := SomeAddress;  { также указывает на

    SomeNumber }

   AnotehrAddress^ := 99;   { новое значение для SomeNumber }

Если вы поймете разницу между этими  двумя  операторами,  то  поймете основные моменты в использовании указателей.  Первый оператор присваивает адрес переменной  AnotherAddress;  он сообщает  ей, куда нужно указывать.  Второй оператор присваивает новое значение элементу,  на который указывает AnotherAddress. На Рис. 8.1  графически показано, как изменяется переменная.

ЪДДДДДДДДДДїЪДДДДДДДДДДДїЪДДДДДДДДДДДїЪДДДДДДДДДДДї

  SomeNumber і 17 іі17  іі 17 іі 99  і

ГДДДДДДДДДДґГДДДДДДДДДДДґГДДДДДДДДДДДґГДДДДДДДДДДДґ

і неіі  іі  іі  і

  SomeAddress   іопределеноіі@SomeNumberіі@SomeNumberіі@SomeNumberі

ГДДДДДДДДДДґГДДДДДДДДДДДґГДДДДДДДДДДДґГДДДДДДДДДДДґ

    і не ііне  іі  іі  і

  AnotherAddressіопределеноііопределено іі@SomeNumberіі@SomeNumberі

АДДДДДДДДДДЩАДДДДДДДДДДДЩАДДДДДДДДДДДЩАДДДДДДДДДДДЩ

   ^   ^   ^   ^

  SomeNumber := 17;   і   і   і

SomeAddress :=  і   і

  @SomeNumber;  і   і

 AnotherAddress  і

  := SomeAddress:   і

AnotherAddress^

:= 99;

Теперь вы получили достаточно хорошее представление о том, в  каких ситуациях  вы можете использовать указатели,  и можно рассмотреть их фактическое применение.  В данном разделе охватываются  следующие темы:

* Распределение динамических переменных.

* Освобождение  выделенной для динамических переменных памяти.

* Распределение и освобождение выделенных объемов памяти.

* Проверка доступного в динамически  распределяемой  области  пространства.

Borland Pascal  предусматривает две пары процедур  для выделения и освобождения памяти,  распределяемой для динамических переменных. Чаще всего используются процедуры New и Dispose,  которые отвечают большинству потребностей. Процедуры GetMem и FreeMem  выполняют те же функции, но на более низком уровне.

Одним из наиболее важных моментов  использования  указателей  является распределение   динамических  переменных  в  динамически  распределяемой области памяти. Borland Pascal предусматривает два  способа выделения для указателя памяти: процедура New и процедура  GetMem.

Использование New как процедуры

New - это очень простая процедура.  После описания  переменной-указателя вы  можете вызвать  процедуру  New для  выделения  пространства в динамически распределяемой памяти для указываемого  переменной элемента. Приведем пример:

var

   IntPointer: ^Integer;

   StringPointer: ^String;

 

begin

   New(IntPointer);  { выделяет в динамически распреде-

 ляемой области два байта }

   New(StringPointer);  { выделяет в динамически распреде-

  .     ляемой области 256 байт }

  .

  .

end.

Пример 8.2 Распределение динамической переменной  с  помощью  процедуры New.

После вызова процедуры New переменная-указатель указывает на  память, выделенную в динамически распределяемой памяти.  В данном  примере IntPointer указывает на двухбайтовую область,  выделенную  процедурой New,  а IntPointer^ - это допустимая целочисленная переменная (хотя  это целочисленное  значение  еще не определено).  Аналогично, StringPointer  указывает на  выделенный  для строки  256-байтовый блок, а его разыменование дает доступную для использования строковую переменную.

  Использование New как функции

Кроме выделения памяти для конкретной динамической  переменной вы  можете использовать New как функцию,  возвращающую указатель конкретного типа. Например, если PInteger - это тип, определенный как ^Integer, а IntPopinter имеет тип PInteger, то следующие два оператора эквивалентны:

New(IntPointer);

IntPointer := New(PInteger);

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

SomeProcedure(New(PointerType));

В этом случае SomeProcedure будет добавлять передаваемый параметр к некоторому списку. В противном случае распределяемая память будет  потеряна.  Библиотеки  Borland Turbo Vision и Borland  Pascal широко используют этот метод для присваивания динамических  объектов спискам.

 

  Использование New с объектами

Когда вы используете New как функцию или процедуру для выделения динамического объекта,  то можете добавить  необязательный  второй параметр,  который задает  применяемый  для инициализации  объекта конструктор. В Примере 8.3 первое обращение к New распределяет пространство  для объекта,  но не инициализирует этот объект. Второй вызов выделяет память и вызывает для задания  объекта  конструктор Init.

type

   PMyObject = ^TMyObject;

   TMyObject = object

   constructor Init;

   end;

 

var

   MyObject, YourObject: PMyObject;

begin

   New(MyObject); { объект не инициализируется }

   New(YourObject, Init);  { вызов Init для инициализации

 объекта }

 

end.

Пример 8.3 Создание динамических объектов.

Примечание: Об объектах и их конструкторах рассказывается в Главе 9 "Объектно-ориентированное программирование".

Память, распределенная  для переменных с помощью New,  после  завершения работы с ними должна освобождаться.  Это позволит  использовать динамически  распределяемую память для других переменных. Чтобы освободить память,  выделенную для динамической  переменной, вы  должны использовать процедуру Dispose.  В Примере 8.2  вы можете добавить следующее:

Dispose(StringPointer);

Dispose(IntPointer);

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

Иногда нежелательно  выделять память тем способом,  как это  делает New.  Вам может потребоваться выделить больше  или  меньше  памяти, чем это делает New по умолчанию,  либо до начала выполнения вы можете просто не знать, сколько памяти вам нужно использовать. Borland Pascal выполняет такое распределение с помощью процедуры GetMem.

Процедура GetMem воспринимает два параметра: переменную-указатель, для которой вы хотите распределить память,  и число распределяемых байт.

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

Пусть, например,  у вас есть прикладная  программа, которая считывает 1000  строк из файла и записывает их в динамическую память. Вы не знаете, насколько длинной будет каждая из этих строк,  поэтому вам  потребуется  описать  строковый тип такого размера,  который будет соответствовать максимальной возможной строке. Если  предположить, что  не все строки имеют максимальную длину, то у  вас будет бесполезно использоваться память.

Чтобы решить эту проблему, вы можете считать каждую строку в  буфер, затем выделить столько памяти,  сколько требуется для фактических данных в строке. Пример этого показан ниже:

type PString = ^String;

 

var

   ReadBuffer: String;

   LinewRead: array[1..1000] of PString;

   TheFile: Text;

   LineNumber: Integer;

 

begin

   Assign(TheFile, 'FOO.TXT');

   Reset(TheFile);

   for LineNumber := 1 to 1000 do

   begin

  Readln(ReadBuffer);

  GetMem(LinesRead[LineNumber], Length(ReadBuffer) + 1);

  LinesRead[LineNumber]^ := ReadBuffer;

   end;

end.

Пример. 8.4 Динамическое распределение памяти для строки.

Вместо выделения для строк 256К (256 символов на строку 1000  раз) вы выделили 4К (4 байта на указатель 1000 раз),  плюс объем,  фактически занимаемый текстом.

Освобождение выделенной памяти

Аналогично тому,  как требуется освобождать память, выделенную с помощью New, вам нужно освобождать память, распределенную с  помощью процедуры  GetMem. Это можно сделать с помощью процедуры  FreeMem. Аналогично тому, как каждому вызову New должен соответствовать парный  вызов Dispose,  каждому  вызову процедуры GetMem  должен соответствовать вызов FreeMem.

Как и GetMem,  процедура FreeMem воспринимает два параметра:  освобождаемую переменную  и объем  освобождаемой памяти.  Важно,  чтобы объем освобождаемой памяти точно совпадал с  объемом  выделенной памяти.  New и  Dispose,  основываясь на типе указателя,  всегда знают,  сколько байт нужно выделять или освобождать.  Но в  случае GetMem и FreeMem объем выделяемой памяти находится всецело  под вашим контролем.

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

Предположим, например,  что вы собираетесь  выделить  память  для одной или более записей данных типа TCheck:

type

   PCheck = ^ TCheck;

   TCheck = record

Amount: Real;

Mounth: 1..12;

Day: 1..31;

Year: 1990..2000;

Payee: string[39];

end.

Пример 8.5 Простой тип записи.

Каждая запись типа TCheck занимает 50 байт,  поэтому, если у  вас есть переменная ThisCheck типа PCheck, вы можете распределить  динамическую запись следующим образом:

GetMem(ThisGheck, 50);

а позднее освободить ее с помощью вызова:

FreeMem(ThisCheck, 50);

Использование с процедурой GetMem функции SizeOf

Однако убедиться, что вы каждый раз выделяете и освобождаете  один и тот же объем памяти,  недостаточно.  Вы должны  обеспечить  распределение правильного объема памяти. Предположим, вы изменили  определение TCheck. Например, если вы переопределили TCheck.Payee  как 50-символьную строку вместо 39-символьной,  то не сможете получить и освобождать достаточно памяти.  Надежнее всего использовать в программе функцию SizeOf, например:

GetMem(ThisCheck, SizeOf(TCheck));

  .

  .

  .

 

FreeMem(ThisCheck, SizeOf(TCheck));

Это не только обеспечивает, что вы выделяете и освобождаете один и  тот же объем,  но гарантирует,  что при изменении размера  типа ваша программа все равно будет выделять нужную память.

В Borland Pascal определены две функции, возвращающие важную  информацию о динамически распределяемой области памяти:  MemAvail  и MaxAvail.

Функция MemAvail возвращает общее число байт,  доступных для  распределения в  динамической  памяти.  Перед выделением большого  объема в динамически распределяемой памяти полезно убедиться, что  такой объем памяти доступен.

Функция MaxAvail  возвращает размер  наибольшего доступного  блока непрерывной памяти в  динамически распределяемой  области.  Первоначально при запуске программы MaxAvail равно MemAvail, поскольку вся динамически  распределяемая  область  памяти  является  доступной и  непрерывной.  После  распределения нескольких блоков  памяти пространство в динамически распределяемой  области  скорее  всего станет фрагментированным.  Это означает,  что между частями  свободного пространства  имеются распределенные  блоки.  Функция  MaxAvail возвращает размер наибольшего свободного блока.

Подробнее о  том, как Borland Pascal работает с динамически  распределяемой областью памяти, рассказывается Главе 21 ("Вопросы  использования памяти") "Руководства по языку".

Указатели позволяют вам делать в  Паскале  некоторые  важные  вещи, но  есть  пара  моментов, которые при работе с указателями  нужно отслеживать.  При использовании указателей допускаются следующие общие ошибки:

- разыменование неинициализированных указателей;

- потери динамически распределяемой памяти ("утечки").

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

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

Использование пустого указателя

Чтобы избежать разыменования указателей, которые не указывают на что-либо значащее,  нужен некоторый способ информирования о  том, что указатель недопустим. В Паскале предусмотрено зарезервированное слово nil, которое вы можете использовать в качестве содержательного значения указателей,  которые в данный момент ни на  что не указывают.  Указатель nil является допустимым, но ни с чем  не связанным. Перед разыменованием указателя вы должны убедиться,  что он отличен от nil (не пуст).

Предположим, например,  что у вас есть функция, возвращающая  указатель на некоторый элемент в памяти.  Вы можете указать,  что  такая функция не может найти элемент, возвращая значение nil.

var ItemPointer: Pointer;

 

function FindIten: Pointer;

begin

   .

   .

   .

{ найти элемент,  возвращая указатель на него или nil,   если элемент не найден }

end;

 

begin

   ItemPointer := nil;   { начнем в предположении nil }

   ItemPointer := FindItem;   { вызвать функцию }

   if ItemPointer <> nil then ... { для надежности разыменования ItemPointer }

end.

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

Общей причиной утечек памяти является переприсваивание динамических переменных без освобождения  предыдущих. Простейшим случаем является следующий:

var IntPointer: ^Integer;

 

begin

New(IntPointer);

New(IntPointer);

end.

Пример 8.6 Простая утечка памяти.

При первом  вызове New  в динамически распределяемой памяти  выделяется 8 байт, и на них устанавливается указатель IntPointer.  Второй вызов New выделяет другие 8 байт,  и IntPointer устанавливается на них. Теперь у вас нет указателя, ссылающегося на первые  8 байт, поэтому вы не можете их освободить. В программе эти байты  будут потеряны.

Естественно, утечка памяти может быть  не  такой  очевидной,  как в Примере 8.6. Выделение памяти почти никогда не происходит в  последовательных операторах,  но может выполняться  в  отдельных  процедурах или далеко отстоящих друг от друга частях программы. В  любом случае лучший способ отслеживания динамических переменных -  это установка их в nil при освобождении.  Тогда при попытке распределить их снова вы можете убедиться что они имеют значение nil:

var IntPointer: ^Integer;

 

begin

New(IntPointer);

   .

   .

   .

Dispose(IntPointer);

IntPointer := nil;

   .

   .

   .

if IntPointer = nil then New(IntPointer);

end.

Предположим, вы хотите написать программу для ведения  своих  личных счетов.  Вы можете хранить все данные о счетах в записях,  таких как запись типа TCheck,  определенная в Примере 8.5. Но при  написании программы трудно предположить, с каким количеством счетов вам придется иметь дело. Одно из решений здесь состоит в создании большого  массива записей счетов,  но это приведет к лишним  затратам памяти. Более элегантное и гибкое решение состоит в расширении определения записи и включении в нее указателя на следующую запись списка,  что приведет к образованию связанного списка,  показанного ниже:

type

   PCheck = ^TCheck;

   TCheck = record

   Amount: Real;

   Month: 1..12;

   Day: 1..31;

   Year: 1990..2000;

   Payee: string[39];

   Next: PCheck;   { указывает на следующую запись }

end.

Пример 8.7 Записи в связанном списке.

Теперь вы можете считать каждую запись счета из файла и  выделить для нее память. Если запись находится в конце списка, поле  Next следует сделать равным nil. В вашей программе требуется отслеживать  только два указателя: первый счет в списке и "текущий"  счет.

Ниже приведена  процедура, которая  строит связанный список  записей, считывая их из файла. Здесь подразумевается, что вы открыли файл записей TCheck и именем CheckFile,  который содержит по  крайней мере одну запись.

var ListChecks, CurrentCheck: PCheck;

 

procedure ReadChecks;

begin

New(ListOfChecks);  { выделить память для первой записи }

Read(CheckFile, ListOfChecks^); { считать первую запись }

CurrentCheck := ListOfChecks;   { сделать первую запись

 текущей }

while not Eof(CheckFile do

begin

  New(CurrentCheck^.Next); { выделить память для

  следующей записи }

   Read(CheckFile, CurrentCheck^.Next^); { считать

  следующую запись }

   CurrentCheck := CurrentCheck^.Next; { сделать следующую

  запись текущей }

end;

CurrentCheck^.Next := nil; { после последней считанной

  записи следующей нет }

end.

 

Пример 8.8 Построение связанного списка.

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

function FindCheckByAmount(AnAmount: Real): PCheck;

var Check: PCheck;

begin

   TempCheck := ListOfChecks;  { указывает на первую запись }

   while (Check^.Amount <> AnAmount) and

(Check^.Next <> nil) do

   Check := Check^.Next;

   if Check^.Amount = AnAmount then

   FindCheckByAmount := Check { возвращает указатель на

     найденную запись }

   else FindCheckByAmount := nil;   { или nil, если таких

 записей нет }

end;

Рис. 8.9 Поиск в связанном списке.

Как показано в процедуре DisposeChecks в  Примере  8.10, вы  можете перебрать  список, дойдя  до каждого элемента и освободив  его.

procedure DisposeChecks;

var Temp: PCheck;

begin

   CurrentCheck := ListOfChecks;  { указывает на первую

  запись }

   while CurrentCheck <> nil do

   begin

  Temp := CurrentCheck^.Next  { сохранить указатель Next }

  Dispose(CurrentCheck);   { освобождение текущей записи }

  CurrentCheck := Temp; { сделать сохраненную запись

 текущей }

   end;

end;

Пример 8.10 Освобождение памяти для связанного списка.