Глава 8. Использование указателей

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

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

  • Зачем и когда используются указатели.
  • Что такое указатель.
  • Как использовать указатели.
  • Эффективная работа с указателями.

Для чего используются указатели?

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

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

(общий объем которых превышает 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.

Процедуры GetMem и FreeMem

Иногда нежелательно выделять память тем способом, как это делает 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;
 
doc/pascal/user_bp/глава_8._использование_указателей.txt · Последнее изменение: d.m.Y H:i — romtek
 
Recent changes RSS feed Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki