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

Демидов Основы программирования в примерах на языке ПАСЦАЛ 2010

.pdf
Скачиваний:
128
Добавлен:
16.08.2013
Размер:
1.28 Mб
Скачать

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

Адреса, указатели и переменные ссылочного типа

Указателями называют переменные, значениями которых являются адреса ячеек памяти.

По сути, адрес ячейки памяти представляет собой число, для представления которого в 32-разрядных системах отводится четыре байта (32 бита), что в сумме даёт 232 = 4 Гб адресного пространства. Для удобства всю память можно логически поделить на равные сегменты, причем старшие два байта адреса будут представлять собой номер сегмента памяти, а младшие два байта – смещение ячейки памяти относительно начала сегмента. Таким образом, об-

щее число сегментов равно 216 при объеме одного сегмента также

216 байт (64 Кб).

Адрес ячейки памяти, выделенной под значение статической переменной, можно узнать по её идентификатору с помощью функции addr(v) или аналогичной унарной операции взятия адреса, обозначаемой символом '@', который записывается слева от идентификатора переменной. Помня о том, что адрес представляется 4- х байтным числом, можно вывести его на экран, предварительно преобразовав его к какому-либо числовому типу. Например:

var i: integer; begin

// адрес статической переменной известен до инициализации writeln(integer(@i));

i := 9; writeln(integer(addr(i)));

end.

141

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

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

Тип указателя указывается при его объявлении. Для типизированного указателя следует записать символ '^', а за ним название типа данных, для работы с которым вводится указатель. Нетипизированные указатели имеют тип pointer. Указатели объявляются в секции переменных. Например:

var

// указатель на значение типа byte

pbyte: ^byte;

prec: ^phone_rec; //

указатель на запись типа phone_rec

p: pointer;

//

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

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

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

Примеры ссылок на статические переменные:

var

 

i: integer;

 

p: pointer;

 

pint: ^integer;

 

begin

// ссылка на переменную i

pint := @i;

p := addr(i);

// здесь равносильно p := pint;

end.

 

142

Для обращения к данным, на которые ссылается указатель, предназначена унарная операция разыменования указателей ^, обозначаемая символом '^', который в этом случае записывается справа от идентификатора указателя. Операция разыменования определена только для типизированных указателей.

Например:

var

i: integer; pint: ^integer;

begin

i := 9;

pint := @i; // Присваиваем указателю адрес переменной i

//В ячейку по адресу, хранимому pint, присваивается 5,

//что в данном случае равносильно инструкции i := 5; pint^ := 5;

//Печатается содержимое ячейки по адресу,

//хранимому pint, что равносильно writeln(i); writeln(pint^);

i := 2 + pint^; // Равносильно i := 2 + i; end.

Таким образом, после выполнения инструкции pint := @i; разыменованный указатель pint^ становится синонимом переменной i до следующего изменения значения pint. По сути, разыменованный указатель представляет собой обычную переменную, только безымянную. В зависимости от адреса (местонахождения в памяти) это будет либо статическая, либо динамическая переменная.

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

143

P1

P2

 

P1

=

P2

 

 

 

 

 

 

 

37

=

37

 

37

 

 

 

 

 

Рис. 10. Разница в сравнении указателей и значений по указателям

Совместимость указателей разных типов

При работе с указателями осуществляется контроль типов. При этом:

1)типизированному указателю можно присваивать значение указателя того же типа, а также значения нетипизированного указателя, при этом происходит неявное преобразование типов;

2)нетипизированному указателю можно присваивать любые значения-адреса, в том числе значения типизированных указателей, при этом информация о типе теряется;

3)любому указателю можно присваивать ссылки на переменные, возвращаемые операцией взятия адреса @ и функцией addr;

4)любому указателю можно присваивать константу NIL,

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

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

Что напечатает следующая программа?

type

pword = ^word; var

p: pword; w: word;

pint: ^integer;

144

begin

//печать адреса переменной pint writeln(integer(@pint));

//ссылка на себя (значение = свой адрес) pint := @pint;

//снова печать собственного адреса writeln(pint^);

//true

writeln(p = @(p^));

// true, приведение типов + разыменование writeln((pword(@w))^ = w);

end.

Если тип значения, хранимого в ячейке памяти, отличается от типа значений типизированного указателя, который на него указывает, то осуществляется автоматическое преобразование типов. При этом возможны аномалии преобразования:

const a: array[1..10] of char ='any string'; var p: ^word;

begin

p := @a;

// преобразование первых двух символов в 2-байтовое число writeln(p^);

end.

В результате будет напечатано число 28257, равное в 16-ричном представлении числу $6E61, где $61 (младший байт) – ASCII-код буквы ‘a’, $6E (старший байт) – ASCII-код буквы ‘n’.

Работа с динамической памятью

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

Для выделения и возврата динамической памяти предназначены следующие процедуры стандартной библиотеки:

145

procedure New(var P: Pointer) – выделяет участок памяти для новой динамической переменной, на которую будет ссылаться указатель P. Размер участка соответствует типу данных, указанному при объявлении P. В результате работы процедуры значением указателя P станет адрес первого байта выделенного участка. Если выделить требуемый объем памяти невозможно, то программа аварийно завершается.

procedure Dispose(var P: Pointer) – удаляет динамическую пе-

ременную, на которую указывает P (возвращает выделенную под неё память системе), созданную с помощью процедуры New.

procedure GetMem(var P: Pointer; Size: Integer) – выделяет участок памяти для новой динамической переменной, на которую будет ссылаться указатель P. Размер участка в байтах равен Size. В результате работы процедуры значением указателя P станет адрес первого байта этого участка. Если выделить требуемый объем памяти невозможно, то программа аварийно завершается. Указатель P может быть любого типа.

procedure FreeMem(var P: Pointer[; Size: Integer]) – удаляет динамическую переменную, на которую указывает P, созданную с помощью процедуры GetMem.

procedure Initialize(var V [ ; Count: Integer ] ) – инициализация участка памяти V длины Count нулевыми значениями. Вызов процедуры уместен в том случае, если память выделялась не с помощью процедуры New.

Пример работы с динамическими переменными:

type

pword = ^word; var

p1: pword;

pr: ^phone_rec; begin

new(p1); p1^ := 2; inc(p1^); dispose(p1);

// выделение памяти под запись и заполнение её полей new(pr);

pr^.fio := 'Ivanov'; pr^.phone := '123-56-78'; pr^.email := 'ivanov@mail.ru';

146

// доступ к полям записи по указателю with pr^ do writeln(fio, phone, email);

dispose(pr); end.

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

Если во время работы программы наступает момент, когда на выделенный участок динамической памяти не ссылается ни один указатель, то участок становится мусором – его адрес программе больше неизвестен. Такой эффект называется утечкой памяти.

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

var

ptr: ^Integer; begin

New(ptr); ptr^ := 53;

ptr := NIL;// с этого момента выделенная память потеряна

end;

Кроме того, попытка вызова Dispose для указателя, который принял значение NIL, приведет к ошибке.

Применение динамической памяти для работы с телефонной книгой

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

147

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

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

Введём необходимые структуры данных:

type

phone_rec = record fio: string[60]; phone: string[20]; e_mail: string[50];

end;

Для размещения этой записи в динамической памяти потребуется переменная-указатель типа ^phone_rec. Но эту структуру необходимо расширить ещё одним полем для хранения указателя на следующую запись, если таковая понадобится. Это поле также должно иметь тип ^phone_rec. Тогда новый тип будет иметь вид

type

phone_rec = record fio: string[60];

148

phone: string[20]; e_mail: string[50]; next: ^phone_rec;

end;

Такой тип данных не является допустимым с точки зрения синтаксиса языка Паскаль, так как одно из полей записи ссылается на тип, еще не обработанный компилятором (рекурсивная ссылка). Для разрешения этого конфликта разрешается объявить новый типуказатель на запись ещё до объявления самого типа-записи, а далее использовать этот тип-указатель внутри объявления типа-записи:

type

pphone_rec = ^phone_rec; phone_rec = record

fio: string[60]; phone: string[20]; e_mail: string[50]; next: pphone_rec;

end;

С помощью этого типа данных можно построить список записей, связывая одну с другой через поле next. Последняя запись не должна ни на что ссылаться, поэтому поле next должно иметь значение пустого указателя NIL.

На рис. 11 показана схема организации списка записей в памяти.

Статическая

переменная-

 

 

 

 

 

указатель

Head

на первую

 

 

 

 

 

запись типа phone_rec

 

 

 

Статическая память

 

 

 

 

 

 

 

 

 

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

 

 

 

 

 

 

 

 

fio

 

Азов Иван

 

 

fio

Боев Антон

 

 

 

 

 

 

 

 

 

phone

 

911-02-03

 

 

phone

290-05-14

 

 

 

 

 

 

 

 

 

 

e_mail

 

az@mail.ru

 

 

e_mail

 

ba@mail.ru

NIL

 

 

 

 

 

 

 

 

 

 

next

 

$ff034517

 

 

next

$df0da302

 

 

 

 

 

 

 

 

 

 

 

 

Рис. 11. Схема организации динамического списка в памяти

149

Таким образом, заранее объявляется лишь одна переменнаяуказатель на головной элемент списка, а сами записи и связи между ними создаются уже в динамической памяти. При этом первая запись ссылается на вторую, вторая – на третью и так далее до NIL. Константа NIL (нулевой адрес) должна завершать любой список, чтобы можно было различить конец списка.

Подпрограммы для работы со списками

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

1)выделить память для новой записи. При этом адрес выделенного участка станет значением какой-либо пере- менной-указателя;

2)заполнить поля записи с помощью операции разыменования;

3)полю next новой записи присвоить адрес первой записи в существующем списке. Этот адрес хранится в статической переменной-указателе Head;

4)адрес новой записи поместить в значение переменной

Head.

Наглядно этот процесс проиллюстрирован на рис. 12. p Head

p Head

Рис. 12. Схема процесса добавления записи в начало списка

150

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]