Алгоритмические языки высокого уровня. Курс лекций
.pdfbegin {Основная программа}
.......
i := MyFunc(1); {Обычное использование результата функции}
.......
VarFunc := MyFunc;
{Присваивание переменной процедурного типа имени функции
MyFunc}
.......
end.
Присваивание VarFunc := MyFunc(1); будет недопусти- мым, так как слева и справа от знака присваивания используются не- совместимые типы: слева – процедурный тип, а справа – INTEGER; имя функции со списком фактических параметров MyFunc(1) трактуется Turbo Pascal как обращение к значению функции, в то время как имя функции без списка параметров рассматривается как имя функции. В отличие от стандартного Pascal, в Turbo Pascal раз- решается использовать в передаваемой процедуре (функции) любые типы параметров: параметры-значения, параметры-переменные, па- раметры-константы (в стандартном Pascal только параметры- значения).
4.4. Указатели, структуры данных
Все переменные, объявленные в программе, размещаются в од- ной непрерывной области оперативной памяти, которая называется сегментом данных. Длина сегмента данных определяется архитекту- рой микропроцессоров 80×86 и составляет 65536 байт (это может привести к трудностям при обработке больших массивов данных). С другой стороны, объем памяти ПК (обычно не менее 640 Кбайт) дос- таточен для успешного решения задач с большой размерностью дан- ных. Для этого может служить использование так называемой дина- мической памяти.
Динамическая память – это оперативная память ПК, предостав- ляемая программе при ее работе, за вычетом сегмента данных (64 Кбайт), стека (обычно 16 Кбайт) и собственно тела программы. Размер динамической памяти можно варьировать в широких преде- лах. По умолчанию этот размер определяется всей доступней памя- тью ПК и, как правило, составляет не менее 200...300 Кбайт.
Динамическая память – это единственное средство обработки дан- ных большой размерности. Многие практические задачи трудно или невозможно решить без использования динамической памяти. Такая
91
необходимость возникает, например, при разработке систем автома- тизированного проектирования (САПР), систем моделирования, вре- менного запоминания данных при работе с графическими и звуко- выми средствами ПК и т.д. Динамическое размещение данных озна- чает использование динамической памяти непосредственно при ра- боте программы. В отличие от этого статическое размещение осуще- ствляется компилятором Pascal в процессе компиляции программы. При динамическом размещении заранее не известны ни тип, ни ко- личество размещаемых данных (к таким данным нельзя обращаться по именам, как к статическим переменным).
Оперативная память ПК представляет собой совокупность эле- ментарных ячеек для хранения информации – байт, каждый из кото- рых имеет собственный номер. Эти номера (ячеек) называются адре- сами (они позволяют обращаться к любому байту/ячейке памяти).
Для работы с ячейками (байтами) динамической памяти исполь- зуются указатели (динамические переменные). Указатель – это пе- ременная, которая в качестве своего значения содержит адрес байта (ячейки) памяти.
В ПК адреса задаются совокупностью двух шестнадцатиразряд- ных слов, которые называются сегментом и смещением. Сегмент – это участок памяти, имеющий длину 65536 байт (64 кбайт) и начи- нающийся с физического адреса, кратного 16 (т.е. 0, 16, 32, 48 и т.д.). Смещение указывает, сколько байт от начала сегмента необ- ходимо пропустить, чтобы обратиться к нужному адресу.
Адресное пространство ПК составляет 1 Мбайт (речь идет о так называемой стандартной памяти ПК; на современных компьюте- рах с процессорами 80386 и выше адресное пространство составляет 4 Гбайт, однако в Turbo Pascal нет средств, поддерживающих работу с дополнительной памятью). Для адресации в пределах 1 Мбайта нужно 20 двоичных разрядов, которые получаются из двух шестна- дцатиразрядных слов (сегмента и смещения) следующим образом: содержимое сегмента смещается влево на 4 разряда, освободив- шиеся правые разряды заполняются нулями, результат складывается с содержимым смещения.
92
Фрагмент памяти в 16 байт называется параграфом, поэтому можно сказать, что сегмент адресует память с точностью до парагра- фа, а смещение – с точностью до байта. Каждому сегменту соответ- ствует непрерывная и отдельно адресуемая область памяти. Сегмен- ты могут следовать в памяти один за другим без промежутков или с некоторым интервалом, или, наконец, перекрывать друг друга. Таким образом, по своей внутренней структуре любой указатель представ- ляет собой совокупность двух слов (данных типа WORD), трактуе- мых как сегмент и смещение. С помощью указателей можно разме- щать в динамической памяти любой из известных в Pascal типов данных. Лишь некоторые из них (BYTE, CHAR, SHORTINT, BOOLEAN) занимают во внутреннем представлении один байт, ос- тальные – несколько смежных байтов. Поэтому на самом деле указа- тель адресует лишь первый байт данных.
4.4.1. Виды указателей
Указатели, связанные с определенными типами данных, называ- ются типизированными. Для объявления типизированного указате- ля используется значок ^, который помещается перед соответствую- щим типом.
var
P1 :^Integer; р2 :^Real;
type
PerconPointer = ^PerconRecord; PerconRecord = record
Name : String;
Job : String;
Next : PerconPointer end;
Замечание: при объявлении типа PerconPointer используется ссылка на тип PerconRecord, который предварительно в про- грамме объявлен не был. В Pascal последовательно проводится в жизнь принцип, в соответствии с которым перед использованием какого-либо идентификатора (переменной) он должен быть описан. Исключение сделано только для указателей, которые могут ссы- латься на еще не объявленный тип данных. Динамическая память дает возможность реализовать широко используемую организацию данных в виде списков. Каждый элемент списка имеет в своем со- ставе указатель на соседний элемент, что обеспечивает возмож-
93
ность просмотра и коррекции списка. Если бы в Pascal не было это- го исключения, реализация списков была бы значительно затруднена.
Указатели, не связанные с конкретными типами данных, называ- ются нетипизированными. Для объявления нетипизированного ука- зателя служит стандартный тип POINTER.
Var
р: pointer;
Поскольку нетипизированные указатели не связаны с конкретным типом, с их помощью удобно динамически размещать данные, струк- тура и тип которых не известны на этапе компиляции и меняются в ходе работы программы. Значениями указателей являются адреса переменных в памяти, но значение одного указателя нельзя переда- вать другому. В Pascal можно передавать значения только между указателями, связанными с одним и тем же типом данных.
Если, например,
var
p1,p2: ^integer; р3 : ^Rеа1;
рр : pointer;
то присваивание p1 := р2; – допустимо,
в то время как р1 := р3; – запрещено, поскольку p1 и p3 указывают на разные типы данных. Это ограничение не распростра- няется на нетипизированные указатели, поэтому можно написать:
pp := p3; p1 := pp; – это будет допустимым.
Замечание: стоило ли вводить ограничения и тут же давать средства для их обхода? Все дело в том, что любое ограничение с одной стороны вводится для повышения надежности программ, а с другой – уменьшает мощность языка программирования, делая его менее пригодным для каких-то применений. В Pascal немногочислен- ные исключения в отношении типов данных придают языку необхо- димую гибкость, но их использование требует от программиста
94
дополнительных усилий и, таким образом, свидетельствует о впол- не осознанном действии.
Для указателей определена константа NIL («ноль»), которая оз- начает, что указатель не указывает никуда:
P:= NIL;
Замечание: если указатель не хранит конкретного адреса (его значение не определено), то это вовсе не означает, что он никуда не указывает. Скорее всего, он все-таки указывает, но только в какую- нибудь неожиданную (хорошо, если не системную) область памяти.
4.4.2. Операции над указателями
Операции присваивания. Для указателей действуют гораздо более жесткие правила совместимости типов, чем для обычных пе- ременных. В операции присваивания могут фигурировать только указатели, адресующие переменные одинаковых типов данных (обойти эти ограничения позволяет универсальность нетипизирован- ного указателя pointer, совместимого с указателями любых ти- пов).
Операции сравнения. Для указателей определены две операции сравнения: '=' и '<>'. Две переменные, описанные как указатели на один и тот же тип данных, считаются совпадающими, если они указывают на одну и ту же область памяти. Для разнотипных указа- телей сравнения невозможны: if p = q then
writeln('yes'); {p: ^byte; q: ^integer} – вызовет ошибку уже на этапе компиляции. Однако сравнивать типизирован- ные и нетипизированные указатели можно.
Операции определения адреса. Физический адрес любой пере-
менной можно узнать с помощью стандартной функции addr(<имя_переменной>):<указатель> или унарной опера-
ции @<имя_переменной>. В зависимости от значения директивы компилятора {$T} результатом операции @ будет либо типизиро- ванный указатель, тип которого будет определен в соответствии с типом использованной переменной ({$T+}), либо нетипизиро- ванный указатель pointer ({$T-}). Результат функции addr() совместим с указателями любых типов: p:= addr(x);
{x: real; p: ^byte}.
Операции разыменования. Для того чтобы воспользоваться зна- чением, хранящимся по некоторому адресу, необходимо его оттуда
95
«извлечь». Унарная операция ^ называется разыменованием и запи- сывается по следующему шаблону: <имя_указателя>^.
Результатом операции ^ является значение, хранящееся по ука- занному адресу. Тип этого значения будет определяться типом (ти- пизированного) указателя. К нетипизированным указателям опера- цию разыменования применять нельзя.
Использование процедуры addr() для разыменования:
const
a: array[1..3] of char ='ААА'; {код(А)=128 или 01000000} var
p: ^word; begin
p:= addr(a); writeln(p^)
end.
На экран будет выведено 32896, что в двоичной системе счис- ления выглядит как 01000000.01000000 (точкой помечена гра- ница двух байтов). Коды двух первых букв оказались слитыми в зна- чение типа word.
Замечание: операции @ и ^ являются взаимно обратными, т.е. для любой переменной a и для любого типизированного указателя p верны следующие равенства:
@(p^)= p и (@a)^ = a.
|
Таблица 4.6 |
|
Пример использования указателя |
|
|
Код |
Описание |
Const i: |
Создается инициализированная переменная i |
integer=1; |
и указатель Pi с неизвестным адресом |
Var Pi: |
|
^integer; |
|
Pi:=@i |
Pi присваивается адрес переменной i |
Pi:=p+^2 |
Значение переменной, адрес которой нахо- |
|
дится в Pi, увеличивается на 2 |
Pi:=nil |
Указателю присваивается нулевое значение |
4.4.3. Процедуры и функции для работы с указателями
Статической переменной (статически размещенной) называется описанная явным образом в программе переменная, обращение к ко- торой осуществляется по имени. Место в памяти для размещения статических переменных определяется при компиляции программы.
96
В отличие от таких статических переменных в программах, написан- ных на языке Pascal, могут быть созданы динамические перемен- ные. Основное свойство динамических переменных заключается в том, что их создание и выделение для них памяти осуществляется во время выполнения программы. Размещаются динамические пере- менные в динамической области памяти (heap-области). Динами- ческая переменная не указывается явно в описаниях переменных, и к ней нельзя обратиться по имени. Доступ к таким переменным осуще- ствляется с помощью указателей и ссылок.
Процедура New (var p: Pointer) – выделяет место в динамической области памяти для размещения динамической переменной p^ и ее адрес присваивает указателю p.
Процедура Dispose (var p: Pointer) – освобождает участок памя- ти, выделенный для размещения динамической переменной проце- дурой New (указатель p становится неопределенным).
Процедура GetMem (var p: Pointer; size: Word) – выделяет уча-
сток памяти в heap-области, присваивает адрес его начала указате- лю p (размер участка в байтах задается параметром size).
Процедура FreeMem (var p: Pointer; size: Word) – освобождает участок памяти, адрес начала определяется указателем p, размер параметром size (указатель p становится неопределенным).
Процедура Mark (var p: Pointer) – записывает в указатель p ад- рес начала участка свободной динамической памяти на момент ее вызова.
Процедура Release (var p: Pointer) – освобождает участок дина- мической памяти, начиная с адреса, записанного в указатель p про- цедурой Mark (очищает ту динамическую память, которая была занята после вызова процедуры Mark).
Функция MaxAvail: Longint – возвращает длину в байтах самого длинного свободного участка динамической памяти.
Функция MemAvail: Longint – возвращает объем свободной ди- намической памяти в байтах.
Функция SizeOf (X): Word – возвращает объем в байтах, зани- маемый X (X – переменная или тип).
Функция OFS(Х): Word – возвращает значение типа WORD, со- держащее смещение адреса указанного объекта (Х – выражение лю- бого типа или имя процедуры).
97
Функция SEG(Х): Word – возвращает значение типа WORD, со- держащее сегмент адреса указанного объекта (X – выражение любого типа или имя процедуры).
Функция PTR(SEG, OFS:word):pointer – функция пре-
вращает адрес, предоставленный в виде <сегмент>:<смещение> в указатель (возвращаемое значение совместимо с указателем лю- бого типа).
Рассмотрим пример работы с указателями
Var p1, p2: ^Integer; { p1 и p2 – указатели или переменные ссылочного типа}
p1:=NIL; p2:=NIL;
После выполнения операторов указатели p1 и p2 не ссылаются ни на какой объект.
New(p1); New(p2);
Процедура New(p1) выполняет следующие действия: в памяти ПК выделяется участок для размещения величины целого типа; адрес это- го участка присваивается переменной p1 (процедура New(p2) обес- печит выделение участка памяти, адрес которого будет записан в p2).
После выполнения операторов присваивания p1^:=2; p2^:=4; в выделенные участки памяти будут записаны значения 2
и4 соответственно.
Врезультате выполнения оператора присваивания p1^:=p2^; в участок памяти, на который ссылается указатель p1, будет записа- но значение 4.
После выполнения оператора присваивания p2:=p1; оба указа- теля будут содержать адрес первого участка памяти
98
Переменные p1^, p2^ являются динамическими, так как па- мять для них выделяется в процессе выполнения программы с помо- щью процедуры New.
Динамические переменные могут входить в состав выражений,
например: p1^:=p1^+8; Write('p1^=',p1^:3);
В результате выполнения программы:
Program DemoPointer; Var p1,p2,p3:^Integer; Begin
p1:=NIL; p2:=NIL; p3:=NIL; New(p1); New(p2); New(p3); p1^:=2; p2^:=4; p3^:=p1^+Sqr(p2^);
writeln('p1^=',p1^:3,' p2^=',p2^:3,' p3^=',p3^:3); p1:=p2;
writeln('p1^=',p1^:3,' p2^=',p2^:3) End.
На экран дисплея будут выведены результаты:
p1^= 2 p2^= 4 p3^= 18 p1^= 4 p2^= 4
Пример программы
Описание: использование динамической памяти для размещения крупных массивов данных.
Пусть требуется обеспечить доступ к элементам прямоугольной матрицы 100×200 типа EXTENDED. Для размещения такого массива требуется память 200000 = 100*200*10 байт, что превышает размер сегмента данных (65536 байт), доступный программе для статическо- го размещения данных. Для решения используется адресная арифме- тика над указателями.
program ptr_array; uses crt;
const
sizeofreal=6; N=100; M=200;
type
Realpointer=^real;
var
i,j: integer;
99
Ptrar:array[1..N] of pointer; s: real;
function AddrR(I,j:word):Realpointer; begin
AddrR:=ptr(seg(PtrAr[i]^),ofs(PtrAr[i]^)+ +(j-1)*Sizeofreal)
end;
function GetR(I,j:integer):real; begin
GetR:=AddrR(i,j)^
end;
procedure PutR(I,j:integer;x:real); begin
AddrR(i,j)^:=x;
end; Begin
clrscr;
for i:=1 to N do begin
Getmem(PtrAr[i],M*Sizeofreal); for j:=1 to M do
putR(i,j,random(100));
end;
s:=0;
for i:=1 to N do for j:=1 to M do
begin s:=s+GetR(i,j);
end;
writeln(‘Sred znach = ‘,s/(n*m):12:2);
End.
4.5. Динамические структуры данных
Абстрактные структуры данных предназначены для удобного хранения и доступа к информации. Они предоставляют удобный ин- терфейс для типичных операций с хранимыми объектами, скрывая детали реализации от пользователя. Абстрактные структуры данных иногда делят на две части: интерфейс, набор операций над объекта-
ми, который называют АТД (абстрактный тип данных) и реализа-
цию (пример использования простых абстрактных структур данных – оперирование с числами. Языки программирования высокого уровня предоставляют удобный интерфейс для чисел: операции +, *, = / и т.п., но при этом скрывают саму реализацию этих операций, ма- шинные команды).
100
