Содержание

Введение в динамические структуры данных


1. Управление памятью в ЯВУ
1.1 Статическая память
1.2 Автоматическая память
1.3 Динамическая память
2. Динамическая память в языке Turbo Pascal
3. Пример создания динамической структуры данных


1. Управление памятью в ЯВУ

Как бы ни была велика основная память современных ЦВМ, программистам её всегда не хватает. В этой связи актуальна задача эффективного её использования. Современные ЯВУ предоставляют программисту несколько механизмов управления памятью.

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

Статическая память выделяется переменным на всё время выполнения программы. Исторически это самый первый механизм "управления" памятью в ЯВУ. Метод прост и не требует никакой поддержки в ходе выполнения программы, так как адреса переменных в этом случае могут быть определены ещё на этапе компиляции программы.

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

program Static;
var i: Integer;
procedure Sub;
const Cnt: Integer = 0;
begin Inc(Cnt); WriteLn('Cnt = ',Cnt) end;
begin
for i:=1 to 3 do Sub;
end.

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

1.2. Автоматическая память

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

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

program Prgm1;
const m = 10000;
var a: array[1..m] of Integer;
    b: array[1..m] of Real;
    i,Sa: Integer;
    Sb:   Real;
begin
{ Ввод массивов a и b }
Sa:=0; Sb:=0;
for i:=1 to m do Sa:=Sa+a[i];
for i:=1 to m do Sb:=Sb+b[i];
WriteLn(Sb*Sa);
end.
program Prgm2;
const m = 10000;
var Sa: Integer;
    Sb: Real;
procedure SumA;
var a: array[1..m] of Integer;
    i: Integer;
begin
{ Ввод массива a }
for i:=1 to m do Sa:=Sa+a[i];
end;
procedure SumB;
var b: array[1..m] of Real;
    i: Integer;
begin
{ Ввод массива b }
for i:=1 to m do Sb:=Sb+b[i];
end;
begin
Sa:=0; Sb:=0;
SumA; SumB;
WriteLn(Sb*Sa);
end.

Не смотря на то, что это абстрактные программы, они обе формально правильные и можно попытаться их выполнить. При попытке выполнить первую программу в среде Turbo Pascal мы получим сообщение об ошибке компиляции "Error 96: Too many variables". Причина ошибки заключается в том, что общий объём памяти, который выделяется переменным уровня главной программы в ситеме Turbo Pascal не должен превышать 64 KB. Учитывая, что переменная типа Integer занимает 2 байта, а типа Real - 6 байтов, то для нашей программы мы получим оценку 10000*(6+2) B = 80000 B = (80000 / 1024) KB = 78.2 KB, что превышает имеющиеся возможности.

Попытка выполнить вторую программу тоже приводит к сообщению об ошибке компиляции - "Error 202: Stack overflow error" (переполнение стека). Так как массивы находятся в разных процедурных блоках программы, а они вызываются последовательно, то это означает, что массивы последовательно используют одну и ту же память в стеке. Оценка необходимого объёма памяти в этом случае составляет 6*10000 B = 58.6 KB - размер большего массива. Причина ошибки состоит в том, что по умолчанию размер стека составляет всего 16 KB, но, к счастью, его можно увеличить до значения 65 520 B, что составляет почти 64 KB. Для этого в интегрированной среде надо выбрать Options | Memory sizes и установить желаемый размер стека. После этого вторая программа будет выполняться без ошибок. Выводы очевидны.

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

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

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

2. Динамическая память в языке Turbo Pascal

Для определения типа указателя на данные базового типа используется следующая конструкция:
type <Имя типа указателя> = ^<Имя базового типа>;
При этом разрешается использовать имя базового типа до его описания.

Переменные-указатели описывается как и обычные в разделе var программы. Для создания и уничтожения динамической переменной используются стандартные процедуры New(p) и Dispose(p) соответственно, где p - переменная типа указатель. Процедура New присваивает переменной p значение указателя на созданную динамическую переменную, а процедура Dispose использует его для её уничтожения.

Над указателями можно выполнять только операции сравнения на = и <> и разименовывания. Типы указателей, участвующих в операции, должны быть одинаковыми. Указатели нельзя использовать в списках ввода-вывода. Операция разименовывания обозначается знаком "^". Так, если p это переменная-указатель, то p^ обозначает динамическую переменную, на которую указывает переменная p. Естественно, что к моменту применения этой операции, динамическая переменная уже должна быть создана. Разрешается присваивать указатели, но только одинакового типа. Существует всего одна константа-указатель - nil. Она обозначает факт отсутствия связи с динамической переменной. Тип этой константы совместим со всеми типами указателей. Рассмотрим пример программы с указателями.

program TestPtr;
type TpInteger = ^Integer;
var p,q: TpInteger; w: Integer;
procedure Wr;
begin WriteLn('p^ = ',p^,' q^ = ',q^) end;
procedure WrMA; begin WriteLn('MemAvail = ',MemAvail) end;
begin
New(p); p^ := 5; New(q); q^ := 7; Wr;
w := p^; p^ := q^; q^ := w; Wr;
p := q; Wr;
Dispose(p); {Dispose(q);}
end.

Выход программы TestPtr:

MemAvail = 299984
p^ = 5 q^ = 7
MemAvail = 299968
p^ = 7 q^ = 5
p^ = 5 q^ = 5
MemAvail = 299976

Этот пример кроме работы с указателями демонстрирует появление динамической переменной (переменная с значением 7), к которой нет доступа. А это означает, что занимаемую ею память нельзя освободить до конца выполнения программы. Такую память называют мусором. Поэтому в программе второй вызов процедуры Dispose закомментирован. Так как значения обоих указателей одинаково, то второй вызов привёл бы к попытке удалить уже удалённую динамическую переменную (с значением 5), что вызывает ошибку. Пример показывает, что с присваиванием динамических переменных надо быть осторожным.

Функция модуля System MemAvail возвращает количество свободных байтов в куче. Поучительно проследить за изменением этого количества в программе TestPtr. Можно заметить, что для одной переменной типа Integer в куче резервируется 8 B! Причина в том, что память в куче резервируется квантами по 8 B. Эти кванты памяти принято называть кластерами. Кластеризация памяти необходима для борьбы с её фрагментацией. Интересно также отметить расхождение количества свободных байтов в куче в начале и в конце выполнения программы - это из-за мусора!

3. Пример создания динамической структуры данных

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

unit Stack;
interface

function Push(v: Integer): Boolean;
function Pop(var v: Integer): Boolean;
function Top(var v: Integer): Boolean;

implementation

type TpTElem = ^TElem;
     TElem = record Info: Integer; Link: TpTElem end;

var sp: TpTElem;

function Push(v: Integer): Boolean;
var p: TpTElem;
begin
if MemAvail < 8 then begin Push:=False; Exit end;
New(p);
p^.Info:=v;
p^.Link:=sp;
sp:=p;
Push:=True;
end; {Push}

function Pop(var v: Integer): Boolean;
var p: TpTElem;
begin
if sp=nil then begin Pop:=False; Exit end;
p:=sp;
sp:=sp^.Link;
v:=p^.Info;
Dispose(p);
Pop:=True;
end; {Pop}

function Top(var v: Integer): Boolean;
var Res: Boolean;
begin
Res:=sp<>nil;
if Res then v:=sp^.Info;
Top:=Res;
end; {Top}

begin sp := nil end.

Пример использования модуля Stack:

program TstStDyn;
uses Stack;
var Cnt: LongInt;
begin
Cnt:=0;
while Push(7) do Inc(Cnt);
WriteLn('Cnt = ',Cnt);
end.

Выход программы TstStDyn:

Cnt = 37398

Принцип организации динамического стека вытекает из структуры его элементов. Каждый элемент стека имеет два поля - поле информации (Info) и поле связи (Link). Поле Info предназначено для хранения значения элемента, а поле Link содержит указатель на следующий элемент стека (предыдущий в порядке поступления в стек). Таким образом указатели связывают элементы стека в единую цепочку. Такую цепочку элементов в информатике называют односвязным линейным списком. Переменная sp - это указатель на вершину стека. Она инициализируется значением nil, что должно указывать на то, что стек пуст.

Суть функции Push состоит в создании нового элемента типа TElem, записи в его поле Link значения sp, что обеспечит связь нового элемента с последующим, и записи в sp указателя на созданный элемент, который тем самым становится вершиной стека.

Суть функции Pop состоит в записи в sp значения поля Link элемента, находящегося   в вершине стека, что приводит к выталкиванию этого элемента из стека. Для освобождения памяти вытолкнутого из стека элемента необходимо предварительно запомнить sp, как его указатель.

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

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

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

Пользуясь случаем, интересно отметить, что интерфейс модуля Stack нашего примера полностью совпадает с интерфейсом модуля Stack из раздела "Модульность". Это обстоятельство является очень полезным на практике, так как позволяет воспользоваться нововведениями без изменения кода модулей-клиентов!


Copyright г Барков Валерий Андреевич, 2000