Реферат: Методические указания для студентов 1 курса факультета математики, механики и компьютерных наук



ФЕДЕРАЛЬНОЕ АГЕНТСТВО ПО ОБРАЗОВАНИЮ


Федеральное государственное образовательное учреждение

высшего профессионального образования

«ЮЖНЫЙ ФЕДЕРАЛЬНЫЙ УНИВЕРСИТЕТ»


С.С. Михалкович


Основы программирования

Указатели. Динамические структуры данных.

Абстрактные типы данных. Классы


МЕТОДИЧЕСКИЕ УКАЗАНИЯ

для студентов 1 курса

факультета математики, механики и компьютерных наук


Ростов-на-Дону

2007


Аннотация


Методические указания содержат лекции по темам «Указатели», «Динамические структуры данных», «Абстрактные типы данных» курса «Основы программирования» для студентов направления «Информационные технологии» факультета математики, механики и компьютерных наук


Методические указания разработаны кандидатом физико-математических наук, доцентом кафедры алгебры и дискретной математики Михалковичем С.С.


Печатается в соответствии с решением кафедры алгебры и дискретной математики факультета математики, механики и компьютерных наук ЮФУ, протокол № 3 от 13 ноября 2006 г.
^ 1Указатели 1.1Общие сведения
Оперативная память компьютера может рассматриваться как массив байтов, индексируемый от нуля. Номер каждого байта в этом массиве называется его адресом. Адресом переменной называется адрес ее первого байта. Для получения адреса переменной в языке Pascal используется унарная операция @: @x – адрес переменной x.

Переменные, в которых хранятся адреса, называются указателями. Любой указатель в 32-разрядной операционной системе занимает 4 байта. Это дает возможность адресовать ячеек памяти. С переходом на 64-битные системы объем адресуемой оперативной памяти станет практически безграничным.

Для чего нужны указатели? Их использование повышает гибкость программирования и разграничивает обязанности: указатель знает лишь адрес переменной, сама переменная может менять свое значение независимо от наличия указателя на нее. Можно провести аналогию между указателями и справочной службой 09. Клиент обращается в справочную службу для того, чтобы узнать номер телефона абонента. Другими словами клиент обращается к указателю, который знает адрес объекта и, следовательно, может вернуть значение этого объекта (в данном случае – номер телефона). Гибкость такого способа очевидна: не следует помнить номера всех телефонов, достаточно знать номер телефона справочной. Кроме того, если номер телефона абонента будет изменен, то в справочной службе будет произведена оперативная корректировка информации, и при последующем обращении в службу клиент получит измененный номер телефона. Другой пример: несколько указателей (банкоматов) указывают на один объект (банковский счет). Посредством разных банкоматов можно снимать деньги с одного банковского счета. Третий пример: файловый указатель, который обращается всякий раз к текущему элементу файла, после чего перемещается на следующий. Это позволяет единым образом (через один указатель) работать с различными данными, находящимися в файле.

В языке Delphi Pascal указатели делятся на типизированные и бестиповые. Если T – некоторый тип, то типизированный указатель на него описываются следующим образом: ^T (указатель на тип). Бестиповой указатель описывается с помощью типа pointer. Если типизированный указатель хранит адрес переменной заданного типа, то бестиповой хранит просто адрес некоторого участка памяти.

Будем изображать тот факт, что указатель pa хранит адрес переменной a, следующим образом:



При этом говорят, что pa указывает на a. Указатель может также хранить специальное значение, задаваемое предопределенной константой nil. Это «нулевое значение» для указателей, означающее, что указатель никуда не указывает. Будем называть такой указатель нулевым и изображать его следующим образом:



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

var a: integer;

r: real;

    pa,pa1: ^integer;

p,p1: pointer;

pr: ^real;

begin

  pa:=@a;

p:=@a;

pa:=p;

p:=pa;

p:=nil;

pa:=nil;

if pa=pa1 then ;

if pa<>p then ;

...

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

pr:=pa; // ошибка: несовместимые типы

if pr=pa then; // ошибка: несовместимые типы

Следует помнить, что в языке Pascal принята именная эквивалентность типов. Поэтому в следующем примере переменные pb и pb1 считаются принадлежащими к разным типам:

var pb: ^integer;

pb1: ^integer;

begin

pb:=pb1; // ошибка компиляции!

if pb<>pb1 then ; // ошибка компиляции!

...

Чтобы можно было присваивать и сравнивать указатели на один и тот же тип, описанные в разных местах, а также передавать указатели как параметры подпрограмм, следует определить новый тип-указатель и описывать переменные-указатели, используя этот тип:

type pinteger=^integer;

var pb: pinteger;

pb1: pinteger;

procedure pr(p: pinteger);

begin

...

end;

...

pb:=pb1; // верно

if pb<>pb1 then ; // верно

pr(pb); // верно

К типизированным указателям применима операция разыменовыния ^ : запись pa^ означает «объект, на который указывает pa» (под объектом здесь понимается область памяти, выделенная программой и трактуемая как переменная или константа определенного типа). В частности, если pa хранит адрес переменной a, то разыменованный указатель pa^ и имя переменной a эквивалентны, поскольку ссылаются на один объект. Вообще, ссылка на объект – это выражение, однозначно определяющее этот объект. В нашем примере имя переменной a и выражение pa^ являются ссылками на один и тот же объект в памяти.

Нулевой указатель и указатель типа pointer разыменовывать нельзя. При разыменовании переменной-указателя, имеющей нулевое значение, произойдет ошибка времени выполнения, разыменование же указателя pointer приведет к ошибке компиляции.

Если типизированный указатель хранит адрес записи или массива, то в Delphi Pascal при обращении через указатель к полю записи или элементу массива операцию разыменования можно не использовать. Например:

type IArr = array [1..100] of integer;

Rec = record i,j: real; end;

var a: IArr; pa: ^IArr;

r: Rec; pr: ^Rec;

begin

pa:=@a;

pr:=@r;

pa[1]:=2; // вместо pa^[1]:=2

pr.i:=3; // вместо pr^.i:=3

end.
^ 1.2Неявные указатели
Указатели неявно встречаются во многих конструкциях языка программирования. Например, при передаче параметра по ссылке в подпрограмму на самом деле передается указатель. Сравним две реализации одной процедуры:

procedure Mult2(var i: integer);

begin

i:=i*2;

end;

procedure Mult2P(pi: pinteger);

begin

pi^:=pi^*2;

end;

var a: integer;

begin

a:=3;

Mult2(a);

Mult2P(@a);

...

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

Другой пример неявных указателей – процедурные переменные. Процедурная переменная хранит адрес процедуры или функции с соответствующей сигнатурой, либо же значение nil (напомним, что сигнатура подпрограммы определяется ее заголовком и включает количество и типы ее параметров, а для функций также и тип возвращаемого значения). Для присваивания процедурной переменной a адреса подпрограммы p с соответствующей сигнатурой знак операции @ использовать необязательно: записи a:=@p и a:=p равнозначны. Например:

type proc = procedure (i: integer);

func = function: real;

var a: proc;

b: func;

procedure p(i: integer);

begin

...

end;

function f: real;

begin

...

end;

begin

a:=@p;

b:=f; // равноценно b:=@f

a(5); // вызов процедуры через процедурную переменную a

writeln(b); // вызов функции через процедурную переменную b

end.
^ 1.3Указатели pointer
Бестиповые указатели pointer хранят адрес памяти, не связанный с объектом определенного типа, и не могут быть разыменованы. Чтобы воспользоваться данными по этому адресу, бестиповой указатель следует преобразовать к указателю на конкретный тип. Например:

type pinteger = ^integer;

preal =^real;

var i: integer;

r: real;

p: pointer;

begin

p:=@i;

pinteger(p)^:=5;

writeln(pinteger(p)^);

p:=@r;

preal(p)^:=3.14;

writeln(preal(p)^);

end.

Рассмотрим запись pinteger(p)^ подробнее. Здесь перед доступом к данным по указателю p мы вначале преобразуем его к указателю на integer, а потом разыменовываем. Поскольку перед обращением к pinteger(p)^ было выполнено присваивание p:=@i, то выражение pinteger(p)^ становится синонимом имени i и может быть использовано как в левой, так и в правой части оператора присваивания.

Гибкость указателей pointer имеет обратную сторону: их применение потенциально опасно и может приводить к ошибкам, причину которых сложно установить. Например, в результате выполнения кода

p:=@i;

preal(p)^:=3.14;

мы обратимся к участку памяти, по которому расположено значение целой переменной i, как к вещественной переменной. Поскольку данные вещественного типа занимают в памяти 8 байт (в Delphi), а данные целого типа – всего 4 байта, то последнее присваивание не только изменит 4 байта, занимаемые переменной i, но и запишет оставшиеся 4 байта в область памяти, следующую за переменной i. Поскольку обычно память под глобальные переменные выделяется подряд в порядке их описания, то оставшиеся 4 байта запишутся в область памяти, отведенную под переменную r (именно она описана вслед за i), то есть в результате последнего присваивания значение переменной r будет испорчено. Подобная ошибка не будет выявлена на стадии компиляции, а при выполнении программы проявится не при данном ошибочном присваивании, а позже, когда мы захотим воспользоваться значением переменной r. Именно поэтому рекомендуется либо отказаться от использования бестиповых указателей, либо при их использовании проявлять предельную аккуратность.

Приведем пример, в котором использование указателей pointer оправдано.

Пример. Внутреннее представление значения real.

Зададимся целью посмотреть, как хранится в памяти переменная типа real. Для этого запишем ее адрес в указатель pointer, после чего преобразуем его в указатель на массив байтов и выведем этот массив на экран.

const sz = sizeof(real);

type Arr=array [1..sz] of byte;

PArr=^Arr;

var r: real;

p: pointer;

pb: PArr;

i: integer;

begin

readln(r);

p:=@r;

pb:=p;

for i:=1 to sz do

write(pb^[i],’ ’);

end.

Отметим одну особенность операции взятия адреса @. В Delphi ее результат зависит от директивы компиляции {$T} («typed @ operator»). По умолчанию установлена директива компиляции {$T-}: это означает, что результат операции @ имеет тип pointer. Если же установлена директива компиляции {$T+}, то результат операции @ – типизированный указатель, базовым типом для которого выступает тип операнда. Кроме того, можно получить адрес переменной, воспользовавшись стандартной функцией Addr(x), которая всегда возвращает значение типа pointer.

Особенностью операции @ можно воспользоваться, чтобы упростить последнее решение. Для этого поставим в начале программы директиву компиляции {$T-}, что позволит нам заменить присваивания p:=@r; pb:=p на pb:=@r и исключить из программы описание переменной p. Подчеркнем, что в режиме {$T+} последнее присваивание приведет к ошибке несоответствия типов, поскольку @r будет возвращать значение типа ^real. Впрочем, в режиме {$T+} можно воспользоваться явным приведением типов (pb:=PArr(@r)) или функцией Addr (pb:=Addr(r)):
^ 1.4Динамическая память и динамические переменные
Память, отводимая под данные программы, делится на статическую, автоматическую и динамическую. Статическая память выделяется до начала работы программы под глобальные переменные и константы и освобождается только при завершении программы. Автоматическая память выделяется на программном стеке под локальные переменные при вызове подпрограммы, а после завершения подпрограммы автоматически освобождается. При этом статическая память инициализируется нулевыми значениями, а автоматическая – не инициализируется (это делается для ускорения вызова подпрограммы).

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

Динамическая память, называемая также кучей, выделяется явно по запросу программы из ресурсов операционной системы и контролируется указателем. Она не инициализируется автоматически и должна быть явно освобождена. В отличие от статической и автоматической памяти динамическая память практически не ограничена (ограничена лишь размером оперативной памяти) и может динамически меняться в процессе работы программы. Недостатки динамической памяти являются продолжением ее достоинств. Во-первых, поскольку она контролируется указателем, доступ к ней осуществляется несколько дольше, чем для статической и автоматической памяти. Во-вторых, программист сам должен заботиться о выделении и освобождении памяти, что чревато большим количеством потенциальных ошибок.
^ 1.5Процедуры New и Delete
Для выделения динамической памяти, контролируемой типизированным указателем, используется стандартная процедура New, для освобождения – стандартная процедура Dispose. Если pt – указатель на тип T, то вызов New(pt) распределяет в динамической памяти переменную типа T и записывает в pt адрес этой переменной:



Переменная, распределенная в динамической памяти, называется динамической переменной. Она не имеет своего имени и для доступа к ней используется разыменованный указатель pt^. После работы с динамической переменной занимаемая ею память должна быть освобождена вызовом стандартной процедуры Dispose, например: Dispose(pt). Таким образом, динамическая переменная существует между вызовами New и Dispose:

var pt: ^real;

begin

  New(pt);

  pt^:=2.8;

pt^:=pt^*2;

...

Dispose(pt);

end.

Выделение и освобождение динамической памяти выполняется специальной подсистемой программы, называемой менеджером кучи. Менеджер кучи хранит список всех незанятых блоков в динамической памяти. При вызове New менеджер кучи ищет незанятый блок подходящего размера, выделяет в нем память и модифицирует список незанятых блоков. При вызове Dispose блок вновь помечается как свободный. После завершения программы вся выделенная для нее динамическая память автоматически возвращается назад системе.

Если динамическая память выделяется в подпрограмме для решения локальных задач данной подпрограммы, то она должна быть освобождена в конце работы этой подпрограммы. Исключение составляют так называемые «создающие» подпрограммы, основным предназначением которых является вернуть объект, созданный в динамической памяти. Например:


function NewInteger(i: integer): pinteger;

begin

New(Result);

Result^:=i;

end;

var pi: pinteger;

begin

pi:= NewInteger(5);

...

При своем вызове функция NewInteger возвращает указатель на динамическую переменную, которая должна быть впоследствии освобождена. Основная проблема состоит в том, что NewInteger не является стандартной функцией, и при ее вызове можно забыть, что она выделяет динамическую память. Один из способов «напомнить» об этом программисту – дать функции имя, свидетельствующее о ее «создающей» способности. Например, имя такой функции может начинаться с префикса New или Create.

Пример. Массив указателей на переменные разных типов.

В некоторых задачах возникает необходимость хранить в массиве данные различных типов. Пусть в массиве требуется хранить данные типа integer, real и shortstring.

Приведем вначале решение, не использубщее указатели.

Решение 1. Используем записи с вариантами. Опишем следующие типы:

type TVar=(tInt,tReal,tStr);

Variant = record

case t: TVar of

tInt: (i: integer);

tReal: (r: real);

tStr: (s: shortstring);

end;

Теперь опишем массив записей Variant и добавим в него несколько значений:

var A: array [1..10] of Variant;

begin

A[1].t:=tInt; A[1].i:=5;

A[2].t:=tReal; A[2].r:=3.14;

A[3].t:=tStr; A[3].s:='Delphi';

end.

Для вывода содержимого массива, очевидно, следует воспользоваться циклом

for i:=1 to 3 do

case A[i].t of

tInt: writeln(A[i].i);

tReal: writeln(A[i].r);

tStr: writeln(A[i].s);

end;

Такое решение имеет важный недостаток: каждый элемент массива имеет размер, определяемый самым большим типом shortstring, что расточительно.

Решение 2. В вариантной части записи Variant будем хранить не значения соответствующих типов, а указатели на них:

type

TVar=(tInt,tReal,tStr);

pinteger=^integer;

preal=^integer;

pshortstring=^shortstring;

Variant = record

t: TVar;

case t: TVar of

tInt: (pi: pinteger);

tReal: (pr: preal);

tStr: (ps: pshortstring);

end;

Будем добавлять в такой массив указатели на переменные разных типов:

var A: array [1..10] of Variant;

begin

A[1].t:=tInt; New(A[1].pi); A[1].pi^:=5;

A[2].t:=tReal; New(A[2].pr); A[2].pr^:=3.14;

A[3].t:=tStr; New(A[3].ps); A[3].ps^:='Delphi';

Для вывода содержимого такого массива воспользуемся следующим циклом:

for i:=1 to 3 do

case A[i].t of

tInt: writeln(pinteger(A[i].p)^);

tReal: writeln(preal(A[i].p)^);

tStr: writeln(pstring(A[i].p)^);

end;

В данном решении суммарный объем данных определяется не размером максимального типа данных, а реальным содержимым в момент выполнения программы. По окончании работы с массивом A динамическую память, занимаемую его элементами, следует освободить. Поскольку параметр процедуры Delete имеет тип pointer, то для освобождения занимаемой памяти можно передать любое из полей-указателей, например, pi:

for i:=1 to 3 do

Delete(A[i].pi);
^ 1.6Процедуры GetMem и FreeMem
Для выделения/освобождения динамической памяти, контролируемой бестиповым указателем, используется другая пара процедур: GetMem и FreeMem. Если p – указатель любого типа (в частности, типа pointer), то вызов GetMem(p,nb) выделяет в динамической памяти участок размера nb байтов и записывает адрес его начала в указатель p. Вызов FreeMem(p) освобождает динамическую память, контролируемую указателем p. Следует обратить внимание, что при вызове FreeMem не указывается размер освобождаемой памяти, поскольку в каждом выделенном блоке хранится его размер, и FreeMem пользуется этой информацией.

В большинстве ситуаций использования типизированных указателей и процедур New и Dispose оказывается достаточно. Процедуры GetMem и FreeMem применяются там, где требуется более гибкое управление памятью.

Пример. Динамический массив.

Динамическим будем называть массив, размер которого задается в процессе работы программы. В Delphi (начиная с версии 4) динамические массивы реализованы средствами языка:

var dyn: array of integer;

n: integer;

begin

read(n);

Assert(n>0);

SetLength(dyn,n);

dyn[0]:=5;

...

Однако, динамические массивы нетрудно создать и с помощью обычных массивов с помощью процедур GetMem и FreeMem:

const sz=MaxInt div sizeof(integer);

type Arr: array [0..sz-1] of integer;

var dyn: ^Arr;

n: integer;

begin

read(n);

Assert(n>0);

GetMem(dyn,n*sizeof(integer));

dyn^[0]:=5; // можно dyn[0]:=5

...

Идея подобной реализации динамического массива состоит в следующем. Описывается тип массива с большим количеством элементов и переменная dyn, являющаяся указателем на этот тип. С помощью GetMem выделяется нужное количество памяти, определяемое в процессе работы программы; адрес выделенной памяти записывается в переменную dyn. С этого момента можно обращаться к элементам массива, используя запись вида dyn^[0]. Операцию разыменования в Delphi можно опускать, поэтому с dyn можно обращаться как с обычным массивом: dyn[0]. В конце работы с таким массивом следует вызвать FreeMem(dyn).

Отметим, что при включенном режиме проверки выхода за границы диапазона {$R+} нельзя выделять под массив память, превосходящую его размер, то есть должно выполняться условие n<=sz. Поэтому следует задавать размер массива sz максимально возможным. В Delphi память, занимаемая переменной любого типа, не должна превосходить 2 Гб, т.е. MaxInt байт. Поскольку элементы массива имеют тип integer, то в качестве sz выбрано максимально возможное значение MaxInt div sizeof(integer).
^ 1.7Ошибки при работе с динамической памятью
Как было отмечено, при работе с динамической памятью можно совершить большое количество ошибок, которые имеют различные последствия и различную степень тяжести. Большинство этих ошибок проявляется не сразу, а через некоторое время в процессе выполнения программы. Следовательно, такие ошибки труднонаходимы и потому особенно опасны. Используя принцип «предупрежден – значит, вооружен», перечислим наиболее часто встречающиеся варианты ошибок при работе с динамической памятью.

1. Попытка воспользоваться неинициализированным указателем.

var pi: ^integer;

    i: integer;

begin

  pi^:=5;

Если pi – глобальная переменная, то она автоматически инициализируется нулевым значением, т.е. имеет значение nil. Разыменование нулевого указателя приводит к ошибке времени выполнения. Если pi – локальная переменная, то она по умолчанию не инициализируется, а поэтому содержит непредсказуемое значение. Это значение трактуется как адрес целой переменной, к которой осуществляется доступ. Как правило, в этой ситуации возникает исключение Access Violation (нарушение защиты доступа), но по чистой случайности может оказаться, что указатель pi содержит истинный адрес переменной программы, тогда переменная будет изменена, выполнение программы продолжится дальше, а факт изменения переменной непредсказуемым образом повлияет на дальнейшее выполнение программы.


2. «Висячие» указатели.

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

var pi: ^integer;

begin

  New(pi);

  pi^:=5;

  Dispose(pi); // указатель становится "висячим"

  ...

  pi^:=6; // ошибка!

Если после Dispose(pi) сразу написать pi:=nil, то в дальнейшем при попытке разыменовать нулевой указатель pi возникнет исключение, что является более предпочтительным, чем скрытая ошибка изменения другой переменной. Данный прием следует взять на вооружение и после освобождения динамической переменной обнулять указатель:

Dispose(pi);

pi:=nil;

3. «Утечка» памяти.

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

Пример 1. Повторное выделение памяти.

Если выделить память повторно для того же указателя, то ранее выделенная память «утечет»:

var pi: ^integer;

begin

  New(pi);

  pi^:=5;

  New(pi);


Пример 2. Выделение памяти под локальную переменную без освобождения.

procedure pp;

var pi: ^integer;

begin

  New(pi);

end;

Данная процедура составлена ошибочно: локальный указатель pi уничтожается после завершения работы процедуры, поэтому контролируемая им динамическая память «утекает». Особенно опасна подобная утечка при вызове такой процедуры в цикле:

for i:=1 to MaxInt do

  pp;

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

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

5. Попытка освободить динамическую память, не выделенную ранее.

var pi: ^integer;

begin

  Dispose(pi);

Подобная ошибка приводит к немедленной генерации исключения, поэтому не принадлежит к числу опасных. Заметим, что в Delphi вызов процедуры Dispose для нулевого указателя просто игнорируется, не приводя к генерации исключения.

6. Попытка дважды освободить занимаемую память.

var pi: ^integer;

begin

  New(pi);

  ...

  Dispose(pi);

  Dispose(pi);

При повторном вызове процедуры Dispose будет сгенерировано исключение.

7. Попытка освободить нединамическую память.

var pi: ^integer;

    i: integer;

begin

  pi:=@i;

  Dispose(pi);

При вызове Dispose для нединамической переменной будет сгенерировано исключение.

8. Выход за память, выделенную процедурой GetMem.

Поскольку GetMem выделяет количество памяти, не зависящее от размера объекта, с которым связан указатель, она менее безопасна, чем New. Например, при реализации с помощью GetMem динамического массива можно предпринять попытку обратиться за границы выделенной памяти:

GetMem(dyn,n*sizeof(integer));

dyn^[n+1]:=5;

Обычно такая ошибка приводит к исключению Access Violation.
^ 2Динамические структуры данных 2.1Общие сведения
Данные могут объединяться в структуры. Примерами структур являются массивы и записи. Но эти структуры во время выполнения программы имеют постоянный размер, то есть являются статическими. Если же размер структуры меняется в процессе работы программы, то такая структура данных называется динамической. Один из способов создать динамическую структуру данных – составить ее из однотипных элементов-записей, каждая из которых, помимо полей данных, имеет поля связи, указывающие на другие элементы. В простейшем случае имеется одно поле связи, которое хранит адрес следующего элемента или nil, если следующий элемент отсутствует.

Попытаемся описать тип элемента такой структуры:

type
  Node=record
    data: integer;
    next: ^Node; // ошибка!
  end;

Данное определение в Delphi Pascal ошибочно. Проблема состоит в том, что тип Node частично определяется через самого себя (рекурсия по данным) и к моменту определения поля next: ^Node тип Node еще не определен полностью. Проблема решается следующим образом: определяется тип PNode=^TNode, после чего определяется тип Node, в котором поле next имеет тип PNode:

type
  PNode=^Node;
  Node=record
    data: integer;
    next: PNode;
  end;

Здесь действует следующее правило Паскаля: в секции type можно описывать указатель на еще не определенный тип при условии, что данный тип будет определен позднее в этой же секции.

Как уже было сказано, элементы типа Node можно завязать в цепочку. Для этого полю next каждого элемента необходимо присвоить указатель на следующий, поле next последнего элемента следует присвоить значение nil (конец цепочки):



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

Если поле next последнего элемента списка указывает на первый элемент, то такая структура называется кольцевым односвязным списком:



В отличие от линейного списка кольцевой не имеет явного начала и конца.

Если у каждого элемента, помимо указателя на следующий, имеется указатель на предыдущий элемент prev: PNode, то такая структура называется линейным двусвязным списком:



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

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


^ 2.2Односвязные линейные списки
Для удобства определим вспомогательную функцию NewNode, создающую в динамической памяти узел линейного односвязного списка, заполняющую его поля значениями data и next и возвращающую указатель на него:

function NewNode(data: integer; next: PNode): PNode;
begin
  New(Result);
  Result^.data:=data;
  Result^.next:=next;
end;

Основными действиями с линейным односвязным списком являются вставка, удаление, поиск элементов, проход по списку и сортировка. Будем считать, что во всех приводимых операциях указатель p типа PNode хранит адрес текущего элемента списка.

Если поле данных data элемента списка содержит значение x, то для простоты будем говорить, что элемент или узел имеет значение x.

1. Вставка элемента со значением x в начало списка.

Пусть первый элемент является текущим (указатель p хранит адрес первого элемента):



Создадим новый элемент со значением x, поле next которого указывает на первый элемент, после чего сделаем этот элемент первым, присвоив его адрес указателю p:

p:=NewNode(x,p);



Отметим, что данная операция производит добавление также и в пустой список (p=nil). При многократном ее выполнении происходит заполнение списка, причем, элементы в списке будут располагаться в порядке, обратном порядку их включения.


2. Удаление элемента из начала непустого списка.



Запомним адрес первого элемента в переменной t, после чего переместим указатель на первый элемент вперед и освободим элемент, контролируемый указателем t:

t:=p;
p:=t^.next;
Dispose(t);



3. Вставка элемента со значением x после текущего.



Данная операция аналогична вставке в начало, но вместо p используется p.next:

p^.next:=NewNode(x,p^.next);




4. Удаление элемента, следующего за текущим.



Данная операция аналогична удалению из начала, но вместо p используется p.next; если p.next=nil, то никаких действий не производится:

t:=p^.next;
if t<>nil then
begin
  p^.next:=t^.next;
  Dispose(t);
end;



5. Вставка элемента со значением x перед текущим.



Вставим после текущего элемента его копию, затем присвоим полю данных текущего элемента значение x и передвинем текущий элемент вперед:

p^.next:=NewNode(p^.data,p^.next);
p^.data:=x;
p:=p^.next;



6. Удаление текущего элемента.



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

t:=p^.next;
p^.data:=t^.data;
p^.next:=t^.next;
Dispose(t);



7. Проход по списку.



Пусть над всеми элементами списка необходимо выполнить некоторую операцию Oper, заданную процедурой с одним ссылочным параметром, например:

procedure Oper(var i: integer);
begin
  i:=i*2;
end;

Установим указатель на его первый элемент и, пока значение указателя не равно nil, будем выполнять операцию Oper и передвигаться на следующий элемент:

while p<>nil do
begin
  Oper(p^.data);
  p:=p^.next;
end;

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

8. Поиск элемента со значением x.



Проход по списку здесь надо совершать либо до конца списка, либо до момента, когда текущий элемент будет иметь значение x. Если после цикла текущий элемент не нулевой, то он содержит первое найденное значение x.

while (p<>nil) and (p^.data<>x) do

  p:=p^.next;

found:=p<>nil;

Следует обратить внимание на то, что порядок следования операндов в операции and важен и для корректной работы последнего алгоритма в Delphi должен быть установлен ключ компиляции сокращенного вычисления логических выражений {$B-}.

9. Разрушение списка.



При разрушении мы удаляем элементы из начала списка до тех пор, пока список не станет пустым:

while p<>nil do

begin

  t:=p;

  p:=p^.next;

  Dispose(t);

end;

10. Вставка элемента в упорядоченный список с сохранением упорядоченности.

Пусть список не пуст и его элементы упорядочены по возрастанию. Чтобы вставить элемент со значением x с сохранением упорядоченности, найдем первый элемент с большим значением и произведем вставку перед ним. Для вставки перед элементом будем «заглядывать» на один элемент вперед, используя вместо переменной p выражение p^.next. Оформим данную операцию в виде процедуры:


procedure SortedListAdd(p: PNode; x: integer);

begin

while (p<>nil) and (p^.next^.data<=x) do
  p:=p^.next;
p^.next:=NewNode(x,p^.next);

end;



Отметим, что наш алгоритм осуществляет добавление после элемента, на который указывает p, поэтому он не может добавить в начало списка. Для решения указанной проблемы поместим в начало списка барьерный элемент, поле данных которого содержит самое маленькое число (-MaxInt). Список при этом сохранит свою упорядоченность, и теперь любой элемент будет добавляться после барьерного. Например, вот как выглядит заполнение упорядоченного списка n случайными числами с последующим его выводом:

readln(n);

p:=NewNode(-MaxInt,nil); // барьер

for i:=1 to n do

SortedListAdd(p,Random(1000));

t:=p;

while t<>nil do

begin

write(t^.data);

t:=t^.next;

end;

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

Таким образом, если в задаче требуется структура с частыми операциями вставки и удаления в середине, следует предпочесть список, если же важнее быстрый доступ по индексу – следует выбрать массив. При выборе структуры следует также учитывать, что для организации односвязного списка на каждый элемент требуется одно поле связи, занимающее 4 байта (для массива подобные накладные расходы отсутствуют). С другой стороны, список может динамически расширяться во время работы программы, в то время как под массив отводится фиксированное количество элементов, которое необходимо указать на этапе компиляции.
^ 2.4Двусвязные линейные списки
Двусвязный линейный список состоит из элементов, каждый из которых хранит адреса следующего и предыдущего. Для удобства работы со списком поддерживаются указатели first и last на первый и последний элемент соответственно:



В структуру Node добавляется указатель prev на предыдующий элемент списка:

type
  PNode=^Node;
  Node=record
    data: integer;
    prev,next: PNode;
  end;

Вспомогательная функция NewNode при этом изменяется очевидным образом:

function NewNode(data: integer; prev, next: PNode): PNode;
begin
  New(Result);
  Result^.data:=data;
  Result^.next:=next;
  Result^.prev:=prev;
end;

Рассмотрим основные операции с двусвязным списком, реализация которых отличает
еще рефераты
Еще работы по разное