ФЕДЕРАЛЬНОЕ АГЕНТСТВО ПО ОБРАЗОВАНИЮ Федеральное государственное образовательное учреждение высшего профессионального об...
17 downloads
356 Views
521KB Size
Report
This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Report copyright / DMCA form
ФЕДЕРАЛЬНОЕ АГЕНТСТВО ПО ОБРАЗОВАНИЮ Федеральное государственное образовательное учреждение высшего профессионального образования «ЮЖНЫЙ ФЕДЕРАЛЬНЫЙ УНИВЕРСИТЕТ»
С.С. Михалкович
Основы программирования Указатели. Динамические структуры данных. Абстрактные типы данных. Классы
МЕТОДИЧЕСКИЕ УКАЗАНИЯ для студентов 1 курса факультета математики, механики и компьютерных наук
Ростов-на-Дону 2007 3
Аннотация Методические указания содержат лекции по темам «Указатели», «Динамические структуры данных», «Абстрактные типы данных» курса «Основы программирования» для студентов направления «Информационные технологии» факультета математики, механики и компьютерных наук Методические указания разработаны кандидатом физико-математических наук, доцентом кафедры алгебры и дискретной математики Михалковичем С.С.
Печатается в соответствии с решением кафедры алгебры и дискретной математики факультета математики, механики и компьютерных наук ЮФУ, протокол № 3 от 13 ноября 2006 г. 4
1 Указатели 1.1 Общие сведения Оперативная память компьютера может рассматриваться как массив байтов, индексируемый от нуля. Номер каждого байта в этом массиве называется его адресом. Адресом переменной называется адрес ее первого байта. Для получения адреса переменной в языке Pascal используется унарная операция @: @x – адрес переменной x. Переменные, в которых хранятся адреса, называются указателями. Любой указатель в 32-разрядной операционной системе занимает 4 байта. Это дает возможность адресовать 2 32 = 4 Гб ячеек памяти. С переходом на 64-битные системы объем адресуемой оперативной памяти станет практически безграничным. Для чего нужны указатели? Их использование повышает гибкость программирования и разграничивает обязанности: указатель знает лишь адрес переменной, сама переменная может менять свое значение независимо от наличия указателя на нее. Можно провести аналогию между указателями и справочной службой 09. Клиент обращается в справочную службу для того, чтобы узнать номер телефона абонента. Другими словами клиент обращается к указателю, который знает адрес объекта и, следовательно, может вернуть значение этого объекта (в данном случае – номер телефона). Гибкость такого способа очевидна: не следует помнить номера всех телефонов, достаточно знать номер телефона справочной. Кроме того, если номер телефона абонента будет изменен, то в справочной службе будет произведена оперативная корректировка информации, и при последующем обращении в службу клиент получит измененный номер телефона. Другой пример: несколько указателей (банкоматов) указывают на один объект (банковский счет). Посредством разных банкоматов можно снимать деньги с одного банковского счета. Третий пример: файловый указатель, который обращается всякий раз к текущему элементу файла, после чего перемещается на следующий. Это позволяет единым образом (через один указатель) работать с различными данными, находящимися в файле. В языке Delphi Pascal указатели делятся на типизированные и бестиповые. Если T – некоторый тип, то типизированный указатель на него описываются следующим образом: ^T (указатель на тип). Бестиповой указатель описывается с помощью типа pointer. Если типизированный указатель хранит адрес переменной заданного типа, то бестиповой хранит просто адрес некоторого участка памяти. Будем изображать тот факт, что указатель pa хранит адрес переменной a, следующим образом: pa
a
5
При этом говорят, что pa указывает на a. Указатель может также хранить специальное значение, задаваемое предопределенной константой nil. Это «нулевое значение» для указателей, означающее, что указатель никуда не указывает. Будем называть такой указатель нулевым и изображать его следующим образом: pa
Типизированные указатели разных типов несовместимы по присваиванию. Однако типизированный и бестиповой указатель совместимы по присваиванию в обе стороны. Указатели одного типа, а также типизированный и бестиповой указатель можно сравнивать на равенство и неравенство. Далее приводятся примеры допустимых действий с указателями: 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 pap then ; ... Следующие действия, наоборот, являются недопустимыми и вызовут ошибку компиляции, поскольку выполняются над указателями, имеющими различный базовый тип: pr:=pa; // ошибка: несовместимые типы if pr=pa then; // ошибка: несовместимые типы Следует помнить, что в языке Pascal принята именная эквивалентность типов. Поэтому в следующем примере переменные pb и pb1 считаются принадлежащими к разным типам: var pb: ^integer; pb1: ^integer; begin pb:=pb1; // ошибка компиляции! if pbpb1 then ; // ошибка компиляции! ... 6
Чтобы можно было присваивать и сравнивать указатели на один и тот же тип, описанные в разных местах, а также передавать указатели как параметры подпрограмм, следует определить новый тип-указатель и описывать переменныеуказатели, используя этот тип: type pinteger=^integer; var pb: pinteger; pb1: pinteger; procedure pr(p: pinteger); begin ... end; ... pb:=pb1; // верно if pbpb1 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. 7
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; 8
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 имеет обратную сторону: их применение потенциально опасно и может приводить к ошибкам, причину которых сложно установить. Например, в результате выполнения кода 9
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+}, то результат операции @ – типизированный указатель, базовым типом для которого 10
выступает тип операнда. Кроме того, можно получить адрес переменной, воспользовавшись стандартной функцией Addr(x), которая всегда возвращает значение типа pointer. Особенностью операции @ можно воспользоваться, чтобы упростить последнее решение. Для этого поставим в начале программы директиву компиляции {$T-}, что позволит нам заменить присваивания p:=@r; pb:=p на pb:=@r и исключить из программы описание переменной p. Подчеркнем, что в режиме {$T+} последнее присваивание приведет к ошибке несоответствия типов, поскольку @r будет возвращать значение типа ^real. Впрочем, в режиме {$T+} можно воспользоваться явным приведением типов (pb:=PArr(@r)) или функцией Addr (pb:=Addr(r)):
1.4 Динамическая память и динамические переменные Память, отводимая под данные программы, делится на статическую, автоматическую и динамическую. Статическая память выделяется до начала работы программы под глобальные переменные и константы и освобождается только при завершении программы. Автоматическая память выделяется на программном стеке под локальные переменные при вызове подпрограммы, а после завершения подпрограммы автоматически освобождается. При этом статическая память инициализируется нулевыми значениями, а автоматическая – не инициализируется (это делается для ускорения вызова подпрограммы). Поскольку как программный стек, так и область статической памяти, выделяются заранее в момент начала работы программы, статическая и автоматическая память имеют фиксированный размер. Однако во многих задачах в разные моменты работы программы требуется существенно различное количество памяти. Отводить для этого фиксированный максимально необходимый размер памяти – расточительство. С данной проблемой мы уже сталкивались при работе с массивами: при описании массива указывается его максимально возможный размер, текущая же заполненность массива, как правило, меньше его размера. Динамическая память, называемая также кучей, выделяется явно по запросу программы из ресурсов операционной системы и контролируется указателем. Она не инициализируется автоматически и должна быть явно освобождена. В отличие от статической и автоматической памяти динамическая память практически не ограничена (ограничена лишь размером оперативной памяти) и может динамически меняться в процессе работы программы. Недостатки динамической памяти являются продолжением ее достоинств. Во-первых, поскольку она контролируется указателем, доступ к ней осуществляется несколько дольше, чем для статической и автоматической памяти. Во-вторых, программист сам должен заботиться о выделении и освобождении памяти, что чревато большим количеством потенциальных ошибок. 11
1.5 Процедуры New и Delete Для выделения динамической памяти, контролируемой типизированным указателем, используется стандартная процедура New, для освобождения – стандартная процедура Dispose. Если pt – указатель на тип T, то вызов New(pt) распределяет в динамической памяти переменную типа T и записывает в pt адрес этой переменной: динамическая память
pt^ pt динамическая переменная
Переменная, распределенная в динамической памяти, называется динамической переменной. Она не имеет своего имени и для доступа к ней используется разыменованный указатель pt^. После работы с динамической переменной занимаемая ею память должна быть освобождена вызовом стандартной процедуры Dispose, например: Dispose(pt). Таким образом, динамическая переменная существует между вызовами New и Dispose: var pt: ^real; begin New(pt); pt^:=2.8; pt^:=pt^*2; ... Dispose(pt); end. Выделение и освобождение динамической памяти выполняется специальной подсистемой программы, называемой менеджером кучи. Менеджер кучи хранит список всех незанятых блоков в динамической памяти. При вызове New менеджер кучи ищет незанятый блок подходящего размера, выделяет в нем память и модифицирует список незанятых блоков. При вызове Dispose блок вновь помечается как свободный. После завершения программы вся выделенная для нее динамическая память автоматически возвращается назад системе. Если динамическая память выделяется в подпрограмме для решения локальных задач данной подпрограммы, то она должна быть освобождена в конце работы этой подпрограммы. Исключение составляют так называемые «создающие» подпрограммы, основным предназначением которых является вернуть объект, созданный в динамической памяти. Например:
12
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); 13
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); 14
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, 15
являющаяся указателем на этот тип. С помощью GetMem выделяется нужное количество памяти, определяемое в процессе работы программы; адрес выделенной памяти записывается в переменную dyn. С этого момента можно обращаться к элементам массива, используя запись вида dyn^[0]. Операцию разыменования в Delphi можно опускать, поэтому с dyn можно обращаться как с обычным массивом: dyn[0]. В конце работы с таким массивом следует вызвать FreeMem(dyn). Отметим, что при включенном режиме проверки выхода за границы диапазона {$R+} нельзя выделять под массив память, превосходящую его размер, то есть должно выполняться условие n