infp-14-m
.pdf«Информатика» для студентов групп Ф2-ХХ
Методические указания
Лабораторная работа 14
Комбинированный тип (запись). Указатели и динамические структуры данных.
Описание типа данных запись
Описание типа
Для работы с комбинациями данных разных типов предназначен комбинированный тип данных – запись. Запись – структурированный тип, состоящий из фиксированного числа компонентов одного или нескольких типов, называемых полями записи. Каждое поле записи имеет свое имя.
Для использования записей, вначале необходимо описать тип записи:
type
ИмяТипа = record ИмяПоля1 : ТипПоля1; ИмяПоля2 : ТипПоля2;
...
ИмяПоляN : ТипПоляN; end;
var
ИмяПеременной : ИмяТипа;
Например, опишем личную карточку успеваемости студента:
type |
|
|
TStudentCard = record |
|
|
SurName |
: string[20]; {фамилия} |
|
Name |
: string[20]; {имя} |
|
BirthYear |
: integer; {год рождения} |
|
HomeAddress |
: string; {домашний адрес} |
|
GroupCode |
: string[7]; {шифр группы} |
|
MathAnal |
: byte; |
{оценка по Мат.анализу} |
LinAlg |
: byte; |
{оценка по Лин.алгебре} |
Phys |
: byte; |
{оценка по Физике} |
Inform |
: byte; |
{оценка по Информатике} |
end; |
|
|
var
st1, st2 : TStudentCard;
Поля-записи
В приведенном примере поля, несущие одинаковую смысловую нагрузку (MathAnal, LinAlg, Phys, Inform), целесообразно объединить в отдельную структуру данных типа запись:
1
type
TStudentCard = record
SurName |
: string[20]; {фамилия} |
Name |
: string[20]; {имя} |
BirthYear |
: integer; {год рождения} |
HomeAddress |
: string; {домашний адрес} |
GroupCode |
: string[7]; {шифр группы} |
Marks : record {оценки} |
|
MathAnal |
: byte; {по Мат.анализу} |
LinAlg |
: byte; {по Лин.алгебре} |
Phys |
: byte; {по Физике} |
Inform |
: byte; {по Информатике} |
end; |
|
end;
А лучше описать следующим образом:
type
TMarks = record {тип оценки}
MathAnal |
: byte; {по |
Мат.анализу} |
LinAlg |
: byte; {по |
Лин.алгебре} |
Phys |
: byte; {по |
Физике} |
Inform |
: byte; {по Информатике} |
|
end; |
|
|
TStudentCard = record |
|
|
SurName |
: string[20]; {фамилия} |
|
Name |
: string[20]; {имя} |
|
BirthYear |
: integer; {год рождения} |
|
HomeAddress : string; |
{домашний адрес} |
|
GroupCode |
: string[7]; {шифр группы} |
|
Marks |
: TMarks; |
{оценки} |
end; |
|
|
Работа с полями
Обращение к полям записей выполняется с помощью составных имен (называемых квалифицируемыми или уточненными идентификаторами), в которых указывается вся цепочка имен от имени переменной типа запись до имени требуемого поля. Имена полей разделяются точками.
Например, пусть для последнего варианта описания карточки студента будут объявлены следующие переменные:
var
stud1, stud2: TStudentCard;
Тогда будут корректными следующие операторы и обращения к полям записей:
begin stud1.Name:=’Сергей’; stud1.BirthYear:=1990; stud1.Marks.Inform:=5; stud2.Marks:=stud1.Marks; stud2:=stud1;
...
end.
2
Массив записей
Работа с массивами записей
Для последнего варианта описания карточки студента объявим тип-массив и переменные-массивы:
type
TGroup = array[1..25] of TStudentCard; var
group1, group2: TGroup;
Тогда будут корректными следующие операторы и обращения к полям записей:
begin
group1[1].SurName:= ’Иванов’; group1[1].Name:= ’Алексей’; group1[1].BirthYear:=1988; group1[1].Marks.MathAnal:=5; group1[1].Marks.Inform:=4;
...
group2[2].Marks:=group1[1].Marks;
...
group2[5]:=group1[3];
...
group1:=group2;
...
end.
Оператор присоединения with
Для упрощения доступа к полям записей и придания программе большей наглядности используется оператор присоединения with. Синтаксис оператора присоединения:
with запись do оператор;
with запись do begin {составной оператор}
оператор; оператор;
...
end;
Например, в предыдущем фрагменте программы можно сократить текст:
with group1[1] do begin SurName:= ’Иванов’; Name:= ’Алексей’; BirthYear:=1988; Marks.MathAnal:=5; Marks.Inform:=4;
end;
3
Распределение памяти и указатели
Динамические переменные и память
При разработке программы не всегда можно точно предсказать реальное количество данных, а, значит, и объем необходимой памяти. Поэтому в большинстве языков программирования имеется возможность создавать и удалять переменные во время выполнения программы. Такие переменные называются динамическими. Они не описываются заранее в разделе описаний и не имеют своего собственного имени, а память под них выделяется по ходу выполнения программы.
Динамические переменные создаются в отдельной специальной области памяти, которая называется динамически распределяемой областью, или
кучей (heap). Работать с переменными в куче можно только через указатели.
Описание указателей
Указатели – это особый тип данных (pointer) для хранения адресов ячеек памяти, в которых находятся другие переменные. Адресом переменной является адрес первого байта ячейки памяти, которая под нее отводится. Для данных структурных типов (массивов и записей) их адресом считается адрес первого байта первого элемента.
Для хранения каждого указателя выделяется 4 байта памяти.
В Pascal имеются два различных вида указателей: типизированные и нетипизированные. Типизированный указатель – это указатель на переменную определенного типа (например, целого, строкового или тапа массива). Нетипизированный указатель – адрес первого байта области памяти, в которой может размещаться любая информация вне зависимости от ее типа.
Примеры объявления указателей:
var |
{указатели на переменные целого типа} |
p1,p2: ^integer; |
|
p3: ^string; |
{указатель на строку} |
p: pointer; |
{нетипизированный указатель} |
Тип pointer совместим со всеми типами указателей.
Создание и удаление динамических переменных
Время жизни динамической переменной начинается от момента выделения ей памяти и заканчивается в момент освобождения выделенной памяти. Этим процессом управляет программист, указывая в коде программы следующие команды.
Для типизированных указателей:
|
Выделение памяти. |
new(p); |
|
|
Выделяется столько байтов памяти, сколько требуется |
|
для хранения переменной заданного типа. Указателю p |
|
присваивается адрес первого байта выделенной памяти. |
dispose(p); |
Освобождение памяти. |
|
Освобождается выделенная память, на которую |
|
указывает p. |
|
4 |
Для нетипизированных указателей:
|
Выделение памяти. |
getmem(p,size); |
|
|
Выделяется столько байтов памяти, сколько указано в |
|
параметре size. Указателю p присваивается адрес |
|
первого байта выделенной области. |
freemem(p,size); |
Освобождение памяти. |
|
Освобождается size байтов, начиная с адреса p. |
Память, выделенная для динамических переменных, должна быть доступна на протяжении всего времени выполнения программы. Поэтому программисту необходимо следить за тем, чтобы ссылки на каждую выделенную ячейку памяти не терялись.
Динамические переменные не имеют собственного имени, поэтому обращаться к ним нужно через имена их указателей. Для доступа к данным по указателю используется унарная операция разыменования, обозначаемая знаком ^.
Для указателей, которые не хранят ни каких адресов, введена константа
пустой (нулевой) адрес с именем nil.
Состояние указателя
Переменная-указатель может находиться в одном из трех состояний:
содержать адрес переменной, память под которую уже выделена;
содержать пустой адрес nil;
находиться в неопределенном состоянии.
Внеопределенном состоянии указатель находится в начале работы программы до первого присваивания ему конкретного адреса или пустого адреса
nil, а также после освобождения области памяти, на которую он указывает.
Операции с указателями
Примеры допустимых операций:
(Дляописанныхвышеуказателей)
p1:=p2;
p:=p3; p1:=p; p2:=nil; p:=nil; var x,y:integer;
... px:^integer; px:=@x;
...
y:=px^;{y=x} p1^:=5; p2^:=2; x:=p1^+3*p2^+4;{x=15}
var b:boolean;
...
b:=p1<>p2;
if p1=nil then ...
Присваивание ( □:=□ )
Указателю допускается присвоить значения только того же типа или типа pointer, а также значение nil.
Получение адреса ( @□ )
Результат унарной операции @ – указатель типа pointer, который можно присвоить любому указателю.
Разыменование ( □^ )
Результат унарной операции ^ – доступ к переменной,
на которую ссылается указатель. Нетипизированные указатели разыменовывать нельзя.
Отношение ( □=□ , □<>□ )
Проверка равенства = и неравенства <> адресов. Результат логического типа (true, false).
Действия с указателями
Разберем пример простейших действий с указателями:
5
type
pint=^integer; {описание типа указателя}
var |
{описание указателей} |
|
p1,p2: pint; |
||
begin |
{выделение |
1-й ячейки памяти по указателю p1} |
new(p1); |
||
new(p2); |
{выделение |
2-й ячейки памяти по указателю p2} |
p1^:=10; |
{занесение |
данных в 1-ю ячейку} |
p2^:=20; |
{занесение данных во 2-ю ячейку} |
|
p1^:=p2^; |
{копирование данных из 2-й ячейки в 1-ю} |
|
dispose(p1); |
{освобождение 1-й ячейки памяти} |
|
p1:=p2; |
{копирование адреса 2-й ячейки в указатель p1} |
|
|
{Теперь оба указателя ссылаются на 2-ю ячейку.} |
|
p1^:=p1^+p2^+5; {изменение данных во 2-й ячейке: 20+20+5=45 } |
||
p2:=nil; |
{обнуление указателя p2 (nil - пустой адрес)} |
|
dispose(p1); |
{освобождение 2-й ячейки памяти} |
|
p1:=nil; |
{обнуление указателя p1 (nil - пустой адрес)} |
|
... |
|
|
end. |
|
|
Выделение и освобождение памяти
Необходимо помнить, что выделенная память, ссылка на которую утеряна, недоступна и не сможет быть освобождена во время выполнения программы. Программисту нужно следить, чтобы постоянно существовали указатели, которые хранят адреса всех выделенных ячеек. Иначе программа может заполнить всю динамически распределяемую память, и ее невозможно будет освободить.
... Недопустимы ситуации, когда ссылки на выделенную память теряются: new(p1); {выделение памяти}
p1:=p2; {переопределение указателя}
...
здесь переопределен p1 – единственный указатель, который ссылался на память, выделенную командой new(p1). Ссылка на эту память утеряна.
Еще один пример утери ссылок из-за переопределения указателя: for i:=1 to 100 do new(p);
На каждой новой итерации данного цикла указатель p переопределяется – теряется адрес ячейки, выделенной на предыдущей итерации. При достаточно большом количестве итераций подобный цикл может заполнить всю кучу.
Также важно не забывать вовремя удалять все созданные при выполнении программы динамические переменные, иначе программа может заполнить всю динамически распределяемую память.
После освобождения памяти (dispose(p)) указатель p находится в неопределенном состоянии, т.е. p ссылается на неиспользуемую ячейку.
Поэтому сразу после команды dispose(p) принято обнулять (или
переопределять) указатель p:
...
dispose(p); {освобождение памяти}
p:=nil; {обнуление указателя}
...
Это считается хорошим стилем программирования.
6
Связанные динамические данные
Основные определения
Линейный список (list) – это динамическая структура данных, которые представляют собой совокупность линейно связанных однородных элементов.
Линейный список называется односвязным, если каждый его элемент (кроме последнего) с помощью указателя связывается с одним (следующим) элементом.
В кольцевом списке имеется связь между последним и первым элементами.
Очередь (queue) – частный случай линейного односвязного списка, организованного по принципу “first in, first out” (FIFO, «первым пришел, первым ушел»). Для очереди разрешено только два действия:
добавление элемента в конец (хвост) очереди,
удаление элемента из начала (головы) очереди.
Стек (stack) – частный случай линейного односвязного списка, организованного по принципу “last in, first out” (LIFO, «последним пришел,
первым ушел»). Для стека разрешено добавлять и удалять элементы только с одного конца списка, который называется вершиной (головой) стека.
Организация связей
Высокая гибкость структур, основанных на связанных динамических данных, достигается за счет реализации двух возможностей:
динамическое выделение и освобождение памяти под элементы в любой момент работы программы;
установление связей между любыми двумя элементами с помощью
указателей.
Для организации связей между элементами динамических структур данных требуется, чтобы каждый элемент содержал кроме информационных значений хотя бы один указатель. Поэтому в качестве элементов таких структур следует использовать записи, которые могут объединять в единое целое разнородные данные.
В простейшем случае элемент динамической структуры данных должен состоять из двух полей: информационного (inf) и указательного (link).
Схематичное изображение такой структуры данных:
inf |
inf |
link |
link |
inf |
link |
inf |
nil |
Соответствующее ей объявление в разделе описаний:
type |
= ^TElem; |
{TPtr |
- тип указателя на элемент} |
||
TPtr |
|||||
TElem = record |
{TElem - тип элемента (запись) |
} |
|||
inf |
: integer; |
{ |
inf |
- информационное поле |
} |
link : TPtr; |
{ |
link - указательное поле |
} |
||
end; |
|
|
|
|
|
Правило последовательности описаний в Pascal требует, чтобы каждый идентификатор был описан прежде, чем он будет использоваться для других объявлений. Однако в данном случае это правило не будет выполнено, как бы
7
ни располагались описания типов указателя TPtr и элемента TElem. Поэтому, для описания типов элементов динамических структур данных сделано исключение: тип указателя на элемент может и должен быть описан
перед описанием типа самого элемента.
Очередь
Указатели очереди
Для создания очереди (queue) и работы с ней необходимо иметь как минимум два указателя:
на начало очереди (назовем его BegQ, от begin of queue),
на конец очереди (назовем EndQ – end of queue).
Кроме того, для освобождения памяти удаляемых элементов потребуется дополнительный указатель (назовем его p), он часто используется и в других
ситуациях для удобства работы с очередью.
Создание очереди
1. Исходное состояние.
BegQ |
|
EndQ |
|
p |
BegQ:=nil; |
nil |
|
nil |
|
? |
EndQ:=nil; |
?
2.Выделение памяти под первый элемент очереди.
BegQ |
|
EndQ |
p |
||
|
|
|
|
|
new(p); |
nil |
|
nil |
|
|
|
|
|
|
|
||
|
|
|
|
|
|
|
|
|
inf |
? |
|
|
|
|
link |
|
|
|
|
|
|
|
? |
|
3. |
Занесение данных в первый элемент очереди. |
|||||
BegQ |
|
EndQ |
p |
p^.inf:=3; |
||
nil |
|
|
nil |
|
|
|
|
|
|
|
p^.link:=nil; |
||
|
|
|
|
|
|
|
|
|
|
|
inf |
3 |
|
|
|
|
|
link |
nil |
|
4. |
Установка указателей BegQ и EndQ на созданный первый элемент. |
|||||
BegQ |
|
EndQ |
p |
|
||
|
|
|
|
|
|
|
inf 3 link nil
8
Добавление элемента очереди
1. Исходное состояние. |
|
p |
||||
BegQ |
|
|
|
EndQ |
|
|
|
|
|
|
|
|
? |
|
|
|
|
|
? |
|
2 |
|
4 |
|
3 |
||
|
|
|
|
nil |
|
|
2. Выделение памяти под новый элемент и занесение в него данных.
BegQ |
|
EndQ |
|
p |
new(p); |
|
|
|
|
|
|
|
|
|
|
|
p^.inf:=5; |
|
|
|
|
|
p^.link:=nil; |
2 |
4 |
3 |
5 |
|
|
nil |
nil |
3. Установка связи между последним элементом очереди и новым, а также
перемещение указателя EndQ на новый элемент. |
|
||||||
|
BegQ |
|
EndQ |
|
p |
EndQ^.link:=p; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
EndQ:=p; |
2 |
4 |
3 |
5 |
|
|
|
nil |
Удаление элемента очереди
1. Исходное состояние.
p |
BegQ |
EndQ |
?
? |
2 |
4 |
3 |
5 |
|
|
|
|
nil |
|
2. Извлечение информации из удаляемого элемента в переменную val и |
||||
установка на него вспомогательного указателя p. |
EndQ |
||||
p |
BegQ |
|
|
||
|
|
|
|
val:=BegQ^.inf; |
|
val |
|
|
p:=BegQ; |
||
4 |
3 |
5 |
|||
2 |
2 |
||||
|
|
|
nil |
9
3. Перестановка указателя BegQ на следующий элемент, используя значение поля link удаляемого элемента. Освобождение памяти удаляемого элемента p.
p |
BegQ |
EndQ |
BegQ:=p^.link;
dispose(p);
val |
2 |
4 |
3 |
5 |
|
2 |
|||||
|
|
|
nil |
Стек
Указатели стека
Для работы со стеком (stack) необходимо иметь один указатель на вершину стека (назовем его Top). Также потребуется один дополнительный
указатель (p), который используется для выделения и освобождения памяти элементов стека.
Создание стека
1. Исходное состояние.
Top |
p |
Top:=nil; |
nil ?
?
2.Выделение памяти под первый элемент стека.
Top p
|
|
|
new(p); |
nil |
|
||
|
|
|
inf ?
link
?
3. Занесение данных в первый элемент стека.
Top |
p |
p^.inf:=5; |
|
nil |
|
|
|
|
|
p^.link:=nil; |
inf 5 link nil
10