Презентацию к данной работе Вы можете скачать здесь.
Управлять большим количеством разрозненных объектов достаточно сложно. С этой проблемой можно справиться путем упорядочивания и ранжирования объектов, то есть объединяя общие для нескольких объектов свойства в одном объекте и используя этот объект в качестве базового.
Эту возможность предоставляет механизм наследования. Он позволяет строить иерархии, в которых объекты-потомки получают свойства объектов-предков и могут дополнять их или изменять. Таким образом, наследование обеспечивает возможность повторного использования кода.
Объекты, расположенные ближе к началу иерархии, объединяют в себе наиболее общие черты для всех нижележащих объектов. По мере продвижения вниз по иерархии объекты приобретают все больше конкретных особенностей.
Объект в Паскале может иметь произвольное количество потомков и только одного предка. При описании объекта имя его предка записывается в круглых скобках после ключевого слова object.
Допустим, нам требуется ввести в игру еще один тип персонажей, который должен обладать свойствами объекта ).
type daemon = object (monster)
procedure init(x_, y_, health_, ammo_, magic_ : word);
procedure attack;
procedure draw;
procedure erase;
procedure wizardry;
private
magic : word;
end;
{ ------------------------- реализация методов объекта daemon ----------------- }
procedure daemon.init(x_, y_, health_, ammo_, magic_ : word);
begin
inherited init(x_, y_, health_, ammo_);
color := green;
magic := magic_;
end;
procedure daemon.attack; { --------------------------------- daemon.attack ---- }
begin
if ammo = 0 then exit;
dec(ammo);
if magic > 0 then begin
outtextXY(x + 15, y, 'БУ-БУХ!'); dec(magic); end
else
outtextXY(x + 15, y, 'бу-бух!');
end;
procedure daemon.draw; { ----------------------------------- daemon.draw ---- }
begin
setcolor(color); outtextXY(x, y, '%)');
end;
procedure daemon.erase; { ----------------------------------- daemon.erase ---- }
begin
setcolor(black); outtextXY(x, y, '%)');
end;
procedure daemon.wizardry; { -------------------------------- daemon.wizardry - }
begin
if magic = 0 then exit;
outtextXY(x + 15, y, 'крибле-крабле-бумс!'); dec(magic);
end;
Наследование полей.Унаследованные поля доступны в объекте точно так же, как и его собственные. Изменить или удалить поле при наследовании нельзя.Объект daemon содержит все поля своего предка и одно собственное поле magic, в котором хранится "магическая сила" объекта.
Наследование методов.В потомке объекта можно не только описывать новые методы, но и переопределять существующие. Метод можно переопределить либо полностью, либо дополнив метод предка.
В объекте daemon описан новый метод wizardry, с помощью которого объект применяет свою магическую силу, а метод инициализации init переопределен, потому что количество полей объекта изменилось. Однако необходимость задавать значения унаследованным полям осталась, и соответствующий метод есть в объекте monster, поэтому из нового метода инициализации сначала вызывается старый, а затем выполняются дополнительные действия (присваивание значения полю ammo ).
Вызов метода предка из метода потомка выполняется с помощью ключевого слова inherited (унаследованный). Можно вызвать метод предка и явным образом с помощью конструкции monster.init.
Методы отрисовки draw и erase также переопределены, потому что изображение демона отличается от изображения монстра и, следовательно, формируется другой последовательностью подпрограмм (для простоты представим демона в виде "смайлика").
Переопределен и метод attack: теперь атака выполняется по-разному в зависимости от наличия магической силы.
Чтобы перемещать демона, требуется выполнить те же действия, что записаны в методе move для перемещения монстра: необходимо стереть его изображение на старом месте, обновить координаты и нарисовать на новом месте. На первый взгляд, можно без проблем унаследовать этот метод, а также метод . Так мы и поступим.
Добавим описание объекта daemon в интерфейсную часть модуля monsters, а тексты его методов — в раздел реализации. Проверим работу новых методов с помощью программы:
program test_inheritance;
uses graph, crt, monsters;
var Vasia : daemon;
gd, gm : integer;
begin
gd := detect; initgraph(gd, gm, '...');
if graphresult <> grOk then begin
writeln('ошибка инициализации графики'); exit end;
Vasia.init(100, 100, 20, 10, 6);
Vasia.draw; Vasia.attack;
readln;
Vasia.erase;
readln;
end.
И в предке, и в потомке есть одноименные методы. Вызывается всегда тот метод, который соответствует типу объекта, потому что при вызове указывается имя экземпляра заданного типа (рис 7.1). Это можно рассматривать как простейший вид полиморфизма.
(рис 7.1) Раннее связывание
Продолжим тестирование объекта daemon, вставив в приведенную выше программу перед первой из процедур readln вызовы методов, унаследованных из объекта monster.
Vasia.move(200, 100); Vasia.move(200, 200); Vasia.
Результаты запуска программы разочаровывают: на экране появляется изображение не демона, а монстра — символ @! Значит, из метода move вызываются методы рисования и стирания объекта-предка. Да и метод атаки, вызываемый из , судя по диагностическому сообщению, также принадлежит объекту monster. Чтобы разобраться, отчего это происходит, рассмотрим механизм работы компилятора.
Исполняемые операторы программы в виде инструкций процессору находятся в сегменте кода. Каждая подпрограмма имеет точку входа. Вызов подпрограммы при компиляции заменяется на последовательность команд, которая передает управление в эту точку, а также выполняет передачу параметров и сохранение регистров процессора. Этот процесс называется разрешением ссылок и в других языках чаще всего выполняется не компилятором, а специальной программой —
Таким образом, при компиляции метода ).
Аналогичная ситуация и с методом ).
Этот механизм называется ранним связыванием, так как все ссылки на подпрограммы компилятор разрешает до выполнения программы. Ясно, что с помощью раннего связывания не удастся обеспечить возможность вызова из одной и той же подпрограммы метода то одного объекта, то другого. Это можно сделать только в случае если ссылки будут разрешаться на этапе выполнения программы в момент вызова метода. Такой механизм в Паскале есть: он называется поздним связыванием и реализуется с помощью так называемых виртуальных методов. Но перед тем как заняться их изучением, надо рассмотреть вопрос о совместимости типов объектов.
Паскаль — язык со
Во всех трех случаях совместимость односторонняя: родительскому объекту может быть присвоен экземпляр любого из его потомков, но не наоборот. Это связано с тем, что при присваивании должны быть заполнены все поля, а потомок имеет либо такой же размер, как предок, либо больший.
Например, если определены переменные:
type pmonster = ^monster;
pdaemon = ^daemon;
var m : monster;
d : daemon;
pm : pmonster;
pd : pdaemon;
то приведенные ниже операторы присваивания допустимы:
m := d; pm := pd;
Поля и методы, введенные в потомке, после таких присваиваний недоступны, потому что объекты базового класса не имеют информации о существовании элементов, определенных в производном. Например, обращение pm^.wizardry ошибочно несмотря на то, что на самом деле указатель pm ссылается на объект типа daemon.
Даже если метод переопределен в потомке, через указатель на предка вызывается метод, описанный в предке. Так, в результате выполнения оператора pm^.draw на экране появится изображение объекта-предка — символ @, потому что тип вызываемого метода соответствует типу указателя, а не типу того объекта, на который он ссылается.
Если известно, что указатель на предка на самом деле хранит ссылку на потомка, можно обратиться к элементам, определенным в потомке, с помощью явного преобразования типа, например pdaemon(pm)^.wizardry.
Если объект является параметром подпрограммы, ему может соответствовать аргумент того же типа или типа любого из его потомков, но есть разница между передачей объектов по значению и по адресу.
Параметр, передаваемый по значению, представляет собой копию объекта-аргумента, содержащую только те поля данных и методы, которые имеются в объекте-параметре. Это значит, что при передаче по значению тип аргумента приводится к типу параметра.
При передаче объекта по адресу подпрограмме передается указатель на фактический объект, то есть приведение типов не выполняется. В подпрограмме тип объекта, передаваемого по адресу, может изменяться в зависимости от аргумента.
Если параметр подпрограммы является указателем на объект, передаваемым по значению, то аргумент может быть указателем как на этот же объект, так и на любого из его потомков. Например, если заголовок процедуры имеет вид
procedure checkp(p1 : pmonster; var p2 : pmonster);
первым параметром в нее можно передавать указатели как на объекты типа monster, так и на любые pmonster.
Объекты, фактический тип которых может изменяться во время выполнения программы, называются полиморфными. Полиморфным может быть объект, определенный через указатель или переданный в подпрограмму по адресу.
Полиморфные объекты широко применяются в программах, потому что они обеспечивают гибкость: например, список, содержащий указатели на объект базового класса, может на самом деле хранить ссылки на любые объекты иерархии. Подпрограммы, параметрами которых являются полиморфные объекты, могут без изменений и даже без перекомпиляции использоваться для объектов, о существовании которых при написании подпрограммы еще ничего не было известно.
Полиморфные объекты обычно применяются вместе с виртуальными методами, которые мы рассмотрим в следующем разделе.
При раннем связывании программа, готовая для выполнения, представляет собой структуру, логика выполнения которой жестко определена. Если же требуется, чтобы решение о том, какой из одноименных методов разных объектов иерархии использовать, принималось в зависимости от конкретного объекта, для которого выполняется вызов, то ясно, что заранее связывать жестко эти методы с остальной частью кода нельзя. Следовательно, надо каким-то образом дать знать компилятору, что эти методы будут обрабатываться по-другому. Для этого в Паскале существует директива virtual. Она записывается в заголовке метода, например:
procedure attack; virtual;
Слово virtual в переводе с английского значит "фактический". Объявление метода виртуальным означает, что все ссылки на этот метод будут разрешаться по факту его вызова, то есть не на стадии компиляции, а во время выполнения программы. Этот механизм называется поздним связыванием. Для его реализации необходимо, чтобы адреса виртуальных методов хранились там, где ими можно будет в любой момент воспользоваться, поэтому компилятор формирует для этих методов таблицу виртуальных методов (
Каждый объект во время выполнения программы должен иметь доступ к
Класс, имеющий хотя бы один виртуальный метод, должен содержать конструктор:
type monster = object
constructor init(x_, y_, health_, ammo_ : word);
procedure attack; virtual;
procedure draw; virtual;
procedure erase; virtual;
procedure hit;
procedure move(x_, y_ : word);
private
x, y : word;
health, ammo : word;
color : word;
end;
daemon = object (monster)
constructor init(x_, y_, health_, ammo_, magic_ : word);
procedure attack; virtual;
procedure draw; virtual;
procedure erase; virtual;
procedure wizardry;
private
magic: word;
end;
По ключевому слову constructor компилятор вставляет в начало метода фрагмент, который записывает ссылку на
Конструктор обычно используется для инициализации объекта. В нем выполняется выделение памяти под
Объект может содержать несколько конструкторов. Повторный вызов конструктора вреда программе не наносит, а вот если конструктор вообще не вызвать и попытаться использовать виртуальный метод, поведение программы не определено. Конструктор должен быть вызван для каждого создаваемого объекта. Присваивание одного объекта другому возможно только после конструирования обоих.
Вызов виртуального метода выполняется так: из объекта берется адрес его
(рис 7.2) Позднее связывание
Поскольку связь с
Правила описания виртуальных методов:
virtual в заголовке метода потомка приведет к ошибке.Для иллюстрации работы виртуальных методов используем программу из предыдущей лекции, сменив в ней тип элементов массива с ). Предварительно в модуле monsters методы attack, draw и erase объявим как виртуальные, а в процедурах init заменим ключевое слово procedure на слово constructor.
program game_2;
uses graph, crt, monsters;
const n = 30;
var stado : array [1 .. n] of daemon;
x, y : array [1 .. n] of integer;
gd, gm : integer;
i, j : word;
begin
gd := detect; initgraph(gd, gm, '...');
if graphresult <> grOk then begin
writeln('ошибка инициализации графики'); exit end;
randomize;
for i := 1 to n do begin
stado[i].init(random(600), random(440), random(10), random(8), random(6));
stado[i].draw;
end;
repeat
for i := 1 to n do begin
x[i] := random(600); y[i] := random(440);
stado[i].move(x[i], y[i]);
end;
for i := 1 to n – 1 do
for j := i + 1 to n do
if (abs(x[i] – x[j]) < 15) and (abs(y[i] – y[j]) < 15)
then begin
stado[i].hit; stado[j].hit;
end;
delay(200);
until keypressed;
end.
Единственное изменение, которое пришлось сделать в части исполняемых операторов программы, — добавление еще одного параметра в метод инициализации init. Запустив программу, можно наблюдать процесс самоуничтожения демонов, что свидетельствует о том, что теперь из методов move и , унаследованных из базового класса, вызываются методы attack, draw и erase, определенные в производном классе.
Виртуальные методы незаменимы и при передаче объектов в подпрограммы. В заголовке подпрограммы описывается либо объект базового типа, передаваемый по адресу, либо указатель на этот объект, а при вызове в нее передается объект или указатель производного класса. В этом случае виртуальные методы, вызываемые для объекта из подпрограммы, будут соответствовать типу аргумента, а не параметра.
При описании классов рекомендуется определять как виртуальные те методы, которые в производных классах будут реализовываться по-другому. Если во всех классах иерархии метод будет выполняться одинаково или если в потомках он не потребуется, его лучше определить как статический (обычный). Применение виртуальных методов обеспечивает гибкость и возможность расширения функциональности модуля, но несколько замедляет выполнение программы, поскольку эти методы вызываются через обращение к
Для хранения объектов в программах чаще всего используется динамическая память, поскольку это обеспечивает гибкость программы и эффективное использование памяти. Благодаря расширенной совместимости типов можно описать указатель на базовый класс и хранить в нем ссылку на любой его объект-потомок, что в сочетании с виртуальными методами позволяет единообразно работать с различными классами иерархии. Из объектов или указателей на объекты создают различные динамические структуры.
Для выделения памяти под объекты используются процедура и функция new. Например, если определены указатели:
type pmonster = ^monster;
pdaemon = ^daemon;
var pm : pmonster;
pd : pdaemon;
можно создать объекты с помощью вызовов:
new(pm); { или pm := new(pmonster); }
new(pd); { или pd := new(pdaemon); }
При использовании new в форме процедуры параметром является указатель, а в функцию передается его тип. Так как после выделения памяти объект обычно инициализируют, для удобства определены расширенные формы new с двумя параметрами. На месте второго параметра задается вызов конструктора объекта.
new(pm, init(1, 1, 1, 1); { или pm := new(pmonster, init(1, 1, 1, 1)); }
new(pd, init(1, 1, 1, 1, 1); { или pd := new(pdaemon, init(1, 1, 1, 1, 1)); }
Обращение к методам
pm^.draw; pm^.attack;
С объектами в динамической памяти часто работают через указатели на базовый класс, то есть описывают указатель базового класса, а инициализируют его, создав объект производного класса, например:
pm := new(pdaemon, init(1, 1, 1, 1, 1));
Как уже говорилось, такие объекты называют полиморфными. Они используются для того, чтобы можно было единообразно работать в программе с объектами разных классов. Например, оператор pm^.draw будет автоматически вызывать разные методы в зависимости от того, на объект какого типа в данный момент ссылается указатель pm (это справедливо только для виртуальных методов).
Для освобождения памяти, занятой объектом, применяется процедура Dispose:
Dispose(pm);
При выполнении этой процедуры освобождается количество байтов, равное размеру объекта, соответствующего типу указателя. Следовательно, если на самом деле в указателе хранится ссылка на объект производного класса, который, как известно, может быть больше своего предка, часть памяти не будет помечена как свободная, но доступ к ней будет невозможен, то есть появится мусор (рис 7.3). Второй случай появления мусора возникает при применении процедуры ). Объект, содержащий динамические поля, мы рассмотрим в конце этой лекции.
(рис 7.3) Неверное удаление полиморфного объекта
(рис 7.4) Неверное удаление объекта с динамическими полями
Для корректного освобождения памяти из-под полиморфных объектов следует использовать вместе с процедурой Dispose специальный метод — деструктор. В документации по Borland Pascal ему рекомендуется давать имя done, например:
destructor monster.done; begin end;
Для правильного освобождения памяти деструктор записывается вторым параметром процедуры Dispose.
Dispose(pm, done);
Для простых объектов деструктор может быть пустым, а для объектов, содержащих динамические поля, в нем записываются операторы освобождения памяти для этих полей. В деструкторе можно описывать любые действия, необходимые для конкретного объекта, например закрытие файлов. Исполняемый код деструктора никогда не бывает пустым, потому что компилятор по служебному слову вставляет в конец тела метода операторы получения размера объекта из Dispose, и она освобождает количество памяти, соответствующее
Dispose память из-под объекта не освобождает.Деструкторы рекомендуется делать виртуальными, для того чтобы при вызове всегда выполнялся деструктор, соответствующий типу объекта. Деструкторы обязательно использовать только для динамических полиморфных объектов, однако можно их применять и для
Вариант кода модуля .
unit monsters;
interface
uses Graph;
type pmonster = ^monster;
monster = object
constructor init(x_, y_, health_, ammo_ : word);
procedure attack; virtual;
procedure draw; virtual;
procedure erase; virtual;
procedure hit;
procedure move(x_, y_ : word);
destructor done;
private
x, y : word;
health, ammo : word;
color : word;
end;
pdaemon = ^daemon;
daemon = object (monster)
constructor init(x_, y_, health_, ammo_, magic_ : word);
procedure attack; virtual;
procedure draw; virtual;
procedure erase; virtual;
procedure wizardry;
private
magic : word;
end;
implementation
{ ------------------- реализация методов объекта monster ---------------------- }
constructor monster.init(x_, y_, health_, ammo_ : word);
begin
x := x_; y := y_;
health := health_;
ammo := ammo_;
color := yellow;
end;
procedure monster.attack; { -------------------------------- monster.attack --- }
begin
if ammo = 0 then exit;
dec(ammo); setcolor(color); outtextXY(x + 15, y, 'ба-бах!');
end;
procedure monster.draw; { -------------------------------- monster.draw ----- }
begin
setcolor(color); outtextXY(x, y, '@');
end;
procedure monster.erase; { ---------------------------------- monster.erase --- }
begin
setcolor(black); outtextXY(x, y, '@');
end;
procedure monster.hit; { ---------------------------------- monster.hit ----- }
begin
if health = 0 then exit;
dec(health);
if health = 0 then begin color := red; draw; exit; end;
attack;
end;
procedure monster.move(x_, y_ : word); { --------------------- monster.move --- }
begin
if health = 0 then exit;
erase;
x := x_; y := y_;
draw;
end;
destructor monster.done; { ----------------------------------- monster.done --- }
begin
end;
{ ----------------------- реализация методов объекта daemon ------------------- }
constructor daemon.init(x_, y_, health_, ammo_, magic_ : word);
begin
inherited init(x_, y_, health_, ammo_);
color := green;
magic := magic_;
end;
procedure daemon.draw; { ----------------------------------- daemon.draw ---- }
begin
setcolor(color); outtextXY(x, y, '%)');
end;
procedure daemon.erase; { ----------------------------------- daemon.erase --- }
begin
setcolor(black); outtextXY(x, y, '%)');
end;
procedure daemon.attack; { ---------------------------------- daemon.attack --- }
begin
if ammo = 0 then exit;
dec(ammo);
if magic > 0 then begin
outtextXY(x + 15, y, 'БУ-БУХ!'); dec(magic); end
else outtextXY(x + 15, y, 'бу-бух!');
end;
procedure daemon.wizardry; { -------------------------------- daemon.wizardry - }
begin
if magic = 0 then exit;
outtextXY(x + 15, y, 'крибле-крабле-бумс!'); dec(magic);
end;
end.
Для использования этого модуля не обязательно иметь в распоряжении его полный исходный код — достаточно интерфейсного раздела (и, конечно, файла .tpu ). В программе, использующей этот модуль, можно описывать производные классы, в которых добавлены новые поля и методы и переопределены имеющиеся. При этом новые объекты будут перемещаться с помощью метода, который был написан до их появления!
Если объект предназначается для хранения других объектов, он называется контейнером. Объекты в контейнере могут храниться в виде массива, списка, стека или другой динамической структуры. Методы контейнера обычно включают его создание, дополнение, просмотр, а также поиск и удаление элементов.
В качестве примера контейнера рассмотрим объект list, предназначенный для работы со связным списком объектов класса monster и его потомков:
type list = object
constructor init;
procedure add(pm : pmonster);
procedure draw;
destructor done;
private
beg : pnode;
end;
В объекте одно поле beg — указатель на начало списка элементов типа node:
type pnode = ^node;
node = record
pm : pmonster; { указатель на объект pmonster }
next : pnode; { указатель на следующий элемент списка }
end;
Структура объекта .
(рис 7.5) Список полиморфных объектов
В программе () создается список из n объектов. Вследствие расширенной совместимости типов методу add может быль передан указатель на любой тип, производный от monster. Объекты выводятся на экран, при этом в одном и том же цикле вызываются разные виртуальные методы draw в соответствии с
program demo_list;
uses graph, crt, monsters;
const n = 30;
type pnode = ^node;
node = record
pm : pmonster;
next : pnode;
end;
list = object
constructor init;
procedure add(pm : pmonster);
procedure draw;
destructor done;
private
beg : pnode;
end;
constructor list.init; { --------------------------------------- list.init ---- }
begin beg := nil end;
procedure list.add(pm : pmonster); { --------------------------- list.add ----- }
var p : pnode;
begin
new(p);
p^.pm := pm;
p^.next := beg;
beg := p;
end;
procedure list.draw; { --------------------------------------- list.draw ---- }
var p : pnode;
begin
p := beg;
while p <> nil do begin
p^.pm^.draw;
p := p^.next;
end;
end;
destructor list.done; { --------------------------------------- list.done ---- }
var p : pnode;
begin
while beg <> nil do begin
p := beg;
dispose(p^.pm, done); { 1 }
beg := p^.next; { 2 }
dispose(p); { 3 }
end
end;
procedure report(message: string); { --------------------------- report ------- }
var s : string;
begin
str(MemAvail, s);
outtext(message + s);
moveto(0, GetY + 12);
end;
var stado : list;
x, y : integer;
gd, gm : integer;
p : pmonster;
i : word;
{ ---------------------------------- главная программа ------------------------ }
begin
gd := detect; initgraph(gd, gm, '...');
if graphresult <> grOk then begin
writeln('ошибка инициализации графики'); exit end;
randomize;
report(' доступно в начале программы: ');
stado.init;
for i := 1 to n do begin
case random(2) of
0 : p := new(pmonster, init(random(600), random(440), 10, 8));
1 : p := new(pdaemon, init(random(600), random(440), 10, 8, 6));
end;
stado.add(p); { добавление объекта в список }
end;
report(' доступно после выделения памяти: ');
stado.draw; { отрисовка объектов }
stado.done; { уничтожение объектов }
report(' доступно после освобождения памяти: ');
readln;
end.
Подробные пояснения к этой программе приведены в учебнике .
Презентацию к данной работе Вы можете скачать здесь.
Управлять большим количеством разрозненных объектов достаточно сложно. С этой проблемой можно справиться путем упорядочивания и ранжирования объектов, то есть объединяя общие для нескольких объектов свойства в одном объекте и используя этот объект в качестве базового.
Эту возможность предоставляет механизм наследования. Он позволяет строить иерархии, в которых объекты-потомки получают свойства объектов-предков и могут дополнять их или изменять. Таким образом, наследование обеспечивает возможность повторного использования кода.
Объекты, расположенные ближе к началу иерархии, объединяют в себе наиболее общие черты для всех нижележащих объектов. По мере продвижения вниз по иерархии объекты приобретают все больше конкретных особенностей.
Объект в Паскале может иметь произвольное количество потомков и только одного предка. При описании объекта имя его предка записывается в круглых скобках после ключевого слова object.
Допустим, нам требуется ввести в игру еще один тип персонажей, который должен обладать свойствами объекта ).
type daemon = object (monster)
procedure init(x_, y_, health_, ammo_, magic_ : word);
procedure attack;
procedure draw;
procedure erase;
procedure wizardry;
private
magic : word;
end;
{ ------------------------- реализация методов объекта daemon ----------------- }
procedure daemon.init(x_, y_, health_, ammo_, magic_ : word);
begin
inherited init(x_, y_, health_, ammo_);
color := green;
magic := magic_;
end;
procedure daemon.attack; { --------------------------------- daemon.attack ---- }
begin
if ammo = 0 then exit;
dec(ammo);
if magic > 0 then begin
outtextXY(x + 15, y, 'БУ-БУХ!'); dec(magic); end
else
outtextXY(x + 15, y, 'бу-бух!');
end;
procedure daemon.draw; { ----------------------------------- daemon.draw ---- }
begin
setcolor(color); outtextXY(x, y, '%)');
end;
procedure daemon.erase; { ----------------------------------- daemon.erase ---- }
begin
setcolor(black); outtextXY(x, y, '%)');
end;
procedure daemon.wizardry; { -------------------------------- daemon.wizardry - }
begin
if magic = 0 then exit;
outtextXY(x + 15, y, 'крибле-крабле-бумс!'); dec(magic);
end;
Наследование полей.Унаследованные поля доступны в объекте точно так же, как и его собственные. Изменить или удалить поле при наследовании нельзя.Объект daemon содержит все поля своего предка и одно собственное поле magic, в котором хранится "магическая сила" объекта.
Наследование методов.В потомке объекта можно не только описывать новые методы, но и переопределять существующие. Метод можно переопределить либо полностью, либо дополнив метод предка.
В объекте daemon описан новый метод wizardry, с помощью которого объект применяет свою магическую силу, а метод инициализации init переопределен, потому что количество полей объекта изменилось. Однако необходимость задавать значения унаследованным полям осталась, и соответствующий метод есть в объекте monster, поэтому из нового метода инициализации сначала вызывается старый, а затем выполняются дополнительные действия (присваивание значения полю ammo ).
Вызов метода предка из метода потомка выполняется с помощью ключевого слова inherited (унаследованный). Можно вызвать метод предка и явным образом с помощью конструкции monster.init.
Методы отрисовки draw и erase также переопределены, потому что изображение демона отличается от изображения монстра и, следовательно, формируется другой последовательностью подпрограмм (для простоты представим демона в виде "смайлика").
Переопределен и метод attack: теперь атака выполняется по-разному в зависимости от наличия магической силы.
Чтобы перемещать демона, требуется выполнить те же действия, что записаны в методе move для перемещения монстра: необходимо стереть его изображение на старом месте, обновить координаты и нарисовать на новом месте. На первый взгляд, можно без проблем унаследовать этот метод, а также метод . Так мы и поступим.
Добавим описание объекта daemon в интерфейсную часть модуля monsters, а тексты его методов — в раздел реализации. Проверим работу новых методов с помощью программы:
program test_inheritance;
uses graph, crt, monsters;
var Vasia : daemon;
gd, gm : integer;
begin
gd := detect; initgraph(gd, gm, '...');
if graphresult <> grOk then begin
writeln('ошибка инициализации графики'); exit end;
Vasia.init(100, 100, 20, 10, 6);
Vasia.draw; Vasia.attack;
readln;
Vasia.erase;
readln;
end.
И в предке, и в потомке есть одноименные методы. Вызывается всегда тот метод, который соответствует типу объекта, потому что при вызове указывается имя экземпляра заданного типа (рис 7.1). Это можно рассматривать как простейший вид полиморфизма.
(рис 7.1) Раннее связывание
Продолжим тестирование объекта daemon, вставив в приведенную выше программу перед первой из процедур readln вызовы методов, унаследованных из объекта monster.
Vasia.move(200, 100); Vasia.move(200, 200); Vasia.
Результаты запуска программы разочаровывают: на экране появляется изображение не демона, а монстра — символ @! Значит, из метода move вызываются методы рисования и стирания объекта-предка. Да и метод атаки, вызываемый из , судя по диагностическому сообщению, также принадлежит объекту monster. Чтобы разобраться, отчего это происходит, рассмотрим механизм работы компилятора.
Исполняемые операторы программы в виде инструкций процессору находятся в сегменте кода. Каждая подпрограмма имеет точку входа. Вызов подпрограммы при компиляции заменяется на последовательность команд, которая передает управление в эту точку, а также выполняет передачу параметров и сохранение регистров процессора. Этот процесс называется разрешением ссылок и в других языках чаще всего выполняется не компилятором, а специальной программой —
Таким образом, при компиляции метода ).
Аналогичная ситуация и с методом ).
Этот механизм называется ранним связыванием, так как все ссылки на подпрограммы компилятор разрешает до выполнения программы. Ясно, что с помощью раннего связывания не удастся обеспечить возможность вызова из одной и той же подпрограммы метода то одного объекта, то другого. Это можно сделать только в случае если ссылки будут разрешаться на этапе выполнения программы в момент вызова метода. Такой механизм в Паскале есть: он называется поздним связыванием и реализуется с помощью так называемых виртуальных методов. Но перед тем как заняться их изучением, надо рассмотреть вопрос о совместимости типов объектов.
Паскаль — язык со
Во всех трех случаях совместимость односторонняя: родительскому объекту может быть присвоен экземпляр любого из его потомков, но не наоборот. Это связано с тем, что при присваивании должны быть заполнены все поля, а потомок имеет либо такой же размер, как предок, либо больший.
Например, если определены переменные:
type pmonster = ^monster;
pdaemon = ^daemon;
var m : monster;
d : daemon;
pm : pmonster;
pd : pdaemon;
то приведенные ниже операторы присваивания допустимы:
m := d; pm := pd;
Поля и методы, введенные в потомке, после таких присваиваний недоступны, потому что объекты базового класса не имеют информации о существовании элементов, определенных в производном. Например, обращение pm^.wizardry ошибочно несмотря на то, что на самом деле указатель pm ссылается на объект типа daemon.
Даже если метод переопределен в потомке, через указатель на предка вызывается метод, описанный в предке. Так, в результате выполнения оператора pm^.draw на экране появится изображение объекта-предка — символ @, потому что тип вызываемого метода соответствует типу указателя, а не типу того объекта, на который он ссылается.
Если известно, что указатель на предка на самом деле хранит ссылку на потомка, можно обратиться к элементам, определенным в потомке, с помощью явного преобразования типа, например pdaemon(pm)^.wizardry.
Если объект является параметром подпрограммы, ему может соответствовать аргумент того же типа или типа любого из его потомков, но есть разница между передачей объектов по значению и по адресу.
Параметр, передаваемый по значению, представляет собой копию объекта-аргумента, содержащую только те поля данных и методы, которые имеются в объекте-параметре. Это значит, что при передаче по значению тип аргумента приводится к типу параметра.
При передаче объекта по адресу подпрограмме передается указатель на фактический объект, то есть приведение типов не выполняется. В подпрограмме тип объекта, передаваемого по адресу, может изменяться в зависимости от аргумента.
Если параметр подпрограммы является указателем на объект, передаваемым по значению, то аргумент может быть указателем как на этот же объект, так и на любого из его потомков. Например, если заголовок процедуры имеет вид
procedure checkp(p1 : pmonster; var p2 : pmonster);
первым параметром в нее можно передавать указатели как на объекты типа monster, так и на любые pmonster.
Объекты, фактический тип которых может изменяться во время выполнения программы, называются полиморфными. Полиморфным может быть объект, определенный через указатель или переданный в подпрограмму по адресу.
Полиморфные объекты широко применяются в программах, потому что они обеспечивают гибкость: например, список, содержащий указатели на объект базового класса, может на самом деле хранить ссылки на любые объекты иерархии. Подпрограммы, параметрами которых являются полиморфные объекты, могут без изменений и даже без перекомпиляции использоваться для объектов, о существовании которых при написании подпрограммы еще ничего не было известно.
Полиморфные объекты обычно применяются вместе с виртуальными методами, которые мы рассмотрим в следующем разделе.
При раннем связывании программа, готовая для выполнения, представляет собой структуру, логика выполнения которой жестко определена. Если же требуется, чтобы решение о том, какой из одноименных методов разных объектов иерархии использовать, принималось в зависимости от конкретного объекта, для которого выполняется вызов, то ясно, что заранее связывать жестко эти методы с остальной частью кода нельзя. Следовательно, надо каким-то образом дать знать компилятору, что эти методы будут обрабатываться по-другому. Для этого в Паскале существует директива virtual. Она записывается в заголовке метода, например:
procedure attack; virtual;
Слово virtual в переводе с английского значит "фактический". Объявление метода виртуальным означает, что все ссылки на этот метод будут разрешаться по факту его вызова, то есть не на стадии компиляции, а во время выполнения программы. Этот механизм называется поздним связыванием. Для его реализации необходимо, чтобы адреса виртуальных методов хранились там, где ими можно будет в любой момент воспользоваться, поэтому компилятор формирует для этих методов таблицу виртуальных методов (
Каждый объект во время выполнения программы должен иметь доступ к
Класс, имеющий хотя бы один виртуальный метод, должен содержать конструктор:
type monster = object
constructor init(x_, y_, health_, ammo_ : word);
procedure attack; virtual;
procedure draw; virtual;
procedure erase; virtual;
procedure hit;
procedure move(x_, y_ : word);
private
x, y : word;
health, ammo : word;
color : word;
end;
daemon = object (monster)
constructor init(x_, y_, health_, ammo_, magic_ : word);
procedure attack; virtual;
procedure draw; virtual;
procedure erase; virtual;
procedure wizardry;
private
magic: word;
end;
По ключевому слову constructor компилятор вставляет в начало метода фрагмент, который записывает ссылку на
Конструктор обычно используется для инициализации объекта. В нем выполняется выделение памяти под
Объект может содержать несколько конструкторов. Повторный вызов конструктора вреда программе не наносит, а вот если конструктор вообще не вызвать и попытаться использовать виртуальный метод, поведение программы не определено. Конструктор должен быть вызван для каждого создаваемого объекта. Присваивание одного объекта другому возможно только после конструирования обоих.
Вызов виртуального метода выполняется так: из объекта берется адрес его
(рис 7.2) Позднее связывание
Поскольку связь с
Правила описания виртуальных методов:
virtual в заголовке метода потомка приведет к ошибке.Для иллюстрации работы виртуальных методов используем программу из предыдущей лекции, сменив в ней тип элементов массива с ). Предварительно в модуле monsters методы attack, draw и erase объявим как виртуальные, а в процедурах init заменим ключевое слово procedure на слово constructor.
program game_2;
uses graph, crt, monsters;
const n = 30;
var stado : array [1 .. n] of daemon;
x, y : array [1 .. n] of integer;
gd, gm : integer;
i, j : word;
begin
gd := detect; initgraph(gd, gm, '...');
if graphresult <> grOk then begin
writeln('ошибка инициализации графики'); exit end;
randomize;
for i := 1 to n do begin
stado[i].init(random(600), random(440), random(10), random(8), random(6));
stado[i].draw;
end;
repeat
for i := 1 to n do begin
x[i] := random(600); y[i] := random(440);
stado[i].move(x[i], y[i]);
end;
for i := 1 to n – 1 do
for j := i + 1 to n do
if (abs(x[i] – x[j]) < 15) and (abs(y[i] – y[j]) < 15)
then begin
stado[i].hit; stado[j].hit;
end;
delay(200);
until keypressed;
end.
Единственное изменение, которое пришлось сделать в части исполняемых операторов программы, — добавление еще одного параметра в метод инициализации init. Запустив программу, можно наблюдать процесс самоуничтожения демонов, что свидетельствует о том, что теперь из методов move и , унаследованных из базового класса, вызываются методы attack, draw и erase, определенные в производном классе.
Виртуальные методы незаменимы и при передаче объектов в подпрограммы. В заголовке подпрограммы описывается либо объект базового типа, передаваемый по адресу, либо указатель на этот объект, а при вызове в нее передается объект или указатель производного класса. В этом случае виртуальные методы, вызываемые для объекта из подпрограммы, будут соответствовать типу аргумента, а не параметра.
При описании классов рекомендуется определять как виртуальные те методы, которые в производных классах будут реализовываться по-другому. Если во всех классах иерархии метод будет выполняться одинаково или если в потомках он не потребуется, его лучше определить как статический (обычный). Применение виртуальных методов обеспечивает гибкость и возможность расширения функциональности модуля, но несколько замедляет выполнение программы, поскольку эти методы вызываются через обращение к
Для хранения объектов в программах чаще всего используется динамическая память, поскольку это обеспечивает гибкость программы и эффективное использование памяти. Благодаря расширенной совместимости типов можно описать указатель на базовый класс и хранить в нем ссылку на любой его объект-потомок, что в сочетании с виртуальными методами позволяет единообразно работать с различными классами иерархии. Из объектов или указателей на объекты создают различные динамические структуры.
Для выделения памяти под объекты используются процедура и функция new. Например, если определены указатели:
type pmonster = ^monster;
pdaemon = ^daemon;
var pm : pmonster;
pd : pdaemon;
можно создать объекты с помощью вызовов:
new(pm); { или pm := new(pmonster); }
new(pd); { или pd := new(pdaemon); }
При использовании new в форме процедуры параметром является указатель, а в функцию передается его тип. Так как после выделения памяти объект обычно инициализируют, для удобства определены расширенные формы new с двумя параметрами. На месте второго параметра задается вызов конструктора объекта.
new(pm, init(1, 1, 1, 1); { или pm := new(pmonster, init(1, 1, 1, 1)); }
new(pd, init(1, 1, 1, 1, 1); { или pd := new(pdaemon, init(1, 1, 1, 1, 1)); }
Обращение к методам
pm^.draw; pm^.attack;
С объектами в динамической памяти часто работают через указатели на базовый класс, то есть описывают указатель базового класса, а инициализируют его, создав объект производного класса, например:
pm := new(pdaemon, init(1, 1, 1, 1, 1));
Как уже говорилось, такие объекты называют полиморфными. Они используются для того, чтобы можно было единообразно работать в программе с объектами разных классов. Например, оператор pm^.draw будет автоматически вызывать разные методы в зависимости от того, на объект какого типа в данный момент ссылается указатель pm (это справедливо только для виртуальных методов).
Для освобождения памяти, занятой объектом, применяется процедура Dispose:
Dispose(pm);
При выполнении этой процедуры освобождается количество байтов, равное размеру объекта, соответствующего типу указателя. Следовательно, если на самом деле в указателе хранится ссылка на объект производного класса, который, как известно, может быть больше своего предка, часть памяти не будет помечена как свободная, но доступ к ней будет невозможен, то есть появится мусор (рис 7.3). Второй случай появления мусора возникает при применении процедуры ). Объект, содержащий динамические поля, мы рассмотрим в конце этой лекции.
(рис 7.3) Неверное удаление полиморфного объекта
(рис 7.4) Неверное удаление объекта с динамическими полями
Для корректного освобождения памяти из-под полиморфных объектов следует использовать вместе с процедурой Dispose специальный метод — деструктор. В документации по Borland Pascal ему рекомендуется давать имя done, например:
destructor monster.done; begin end;
Для правильного освобождения памяти деструктор записывается вторым параметром процедуры Dispose.
Dispose(pm, done);
Для простых объектов деструктор может быть пустым, а для объектов, содержащих динамические поля, в нем записываются операторы освобождения памяти для этих полей. В деструкторе можно описывать любые действия, необходимые для конкретного объекта, например закрытие файлов. Исполняемый код деструктора никогда не бывает пустым, потому что компилятор по служебному слову вставляет в конец тела метода операторы получения размера объекта из Dispose, и она освобождает количество памяти, соответствующее
Dispose память из-под объекта не освобождает.Деструкторы рекомендуется делать виртуальными, для того чтобы при вызове всегда выполнялся деструктор, соответствующий типу объекта. Деструкторы обязательно использовать только для динамических полиморфных объектов, однако можно их применять и для
Вариант кода модуля .
unit monsters;
interface
uses Graph;
type pmonster = ^monster;
monster = object
constructor init(x_, y_, health_, ammo_ : word);
procedure attack; virtual;
procedure draw; virtual;
procedure erase; virtual;
procedure hit;
procedure move(x_, y_ : word);
destructor done;
private
x, y : word;
health, ammo : word;
color : word;
end;
pdaemon = ^daemon;
daemon = object (monster)
constructor init(x_, y_, health_, ammo_, magic_ : word);
procedure attack; virtual;
procedure draw; virtual;
procedure erase; virtual;
procedure wizardry;
private
magic : word;
end;
implementation
{ ------------------- реализация методов объекта monster ---------------------- }
constructor monster.init(x_, y_, health_, ammo_ : word);
begin
x := x_; y := y_;
health := health_;
ammo := ammo_;
color := yellow;
end;
procedure monster.attack; { -------------------------------- monster.attack --- }
begin
if ammo = 0 then exit;
dec(ammo); setcolor(color); outtextXY(x + 15, y, 'ба-бах!');
end;
procedure monster.draw; { -------------------------------- monster.draw ----- }
begin
setcolor(color); outtextXY(x, y, '@');
end;
procedure monster.erase; { ---------------------------------- monster.erase --- }
begin
setcolor(black); outtextXY(x, y, '@');
end;
procedure monster.hit; { ---------------------------------- monster.hit ----- }
begin
if health = 0 then exit;
dec(health);
if health = 0 then begin color := red; draw; exit; end;
attack;
end;
procedure monster.move(x_, y_ : word); { --------------------- monster.move --- }
begin
if health = 0 then exit;
erase;
x := x_; y := y_;
draw;
end;
destructor monster.done; { ----------------------------------- monster.done --- }
begin
end;
{ ----------------------- реализация методов объекта daemon ------------------- }
constructor daemon.init(x_, y_, health_, ammo_, magic_ : word);
begin
inherited init(x_, y_, health_, ammo_);
color := green;
magic := magic_;
end;
procedure daemon.draw; { ----------------------------------- daemon.draw ---- }
begin
setcolor(color); outtextXY(x, y, '%)');
end;
procedure daemon.erase; { ----------------------------------- daemon.erase --- }
begin
setcolor(black); outtextXY(x, y, '%)');
end;
procedure daemon.attack; { ---------------------------------- daemon.attack --- }
begin
if ammo = 0 then exit;
dec(ammo);
if magic > 0 then begin
outtextXY(x + 15, y, 'БУ-БУХ!'); dec(magic); end
else outtextXY(x + 15, y, 'бу-бух!');
end;
procedure daemon.wizardry; { -------------------------------- daemon.wizardry - }
begin
if magic = 0 then exit;
outtextXY(x + 15, y, 'крибле-крабле-бумс!'); dec(magic);
end;
end.
Для использования этого модуля не обязательно иметь в распоряжении его полный исходный код — достаточно интерфейсного раздела (и, конечно, файла .tpu ). В программе, использующей этот модуль, можно описывать производные классы, в которых добавлены новые поля и методы и переопределены имеющиеся. При этом новые объекты будут перемещаться с помощью метода, который был написан до их появления!
Если объект предназначается для хранения других объектов, он называется контейнером. Объекты в контейнере могут храниться в виде массива, списка, стека или другой динамической структуры. Методы контейнера обычно включают его создание, дополнение, просмотр, а также поиск и удаление элементов.
В качестве примера контейнера рассмотрим объект list, предназначенный для работы со связным списком объектов класса monster и его потомков:
type list = object
constructor init;
procedure add(pm : pmonster);
procedure draw;
destructor done;
private
beg : pnode;
end;
В объекте одно поле beg — указатель на начало списка элементов типа node:
type pnode = ^node;
node = record
pm : pmonster; { указатель на объект pmonster }
next : pnode; { указатель на следующий элемент списка }
end;
Структура объекта .
(рис 7.5) Список полиморфных объектов
В программе () создается список из n объектов. Вследствие расширенной совместимости типов методу add может быль передан указатель на любой тип, производный от monster. Объекты выводятся на экран, при этом в одном и том же цикле вызываются разные виртуальные методы draw в соответствии с
program demo_list;
uses graph, crt, monsters;
const n = 30;
type pnode = ^node;
node = record
pm : pmonster;
next : pnode;
end;
list = object
constructor init;
procedure add(pm : pmonster);
procedure draw;
destructor done;
private
beg : pnode;
end;
constructor list.init; { --------------------------------------- list.init ---- }
begin beg := nil end;
procedure list.add(pm : pmonster); { --------------------------- list.add ----- }
var p : pnode;
begin
new(p);
p^.pm := pm;
p^.next := beg;
beg := p;
end;
procedure list.draw; { --------------------------------------- list.draw ---- }
var p : pnode;
begin
p := beg;
while p <> nil do begin
p^.pm^.draw;
p := p^.next;
end;
end;
destructor list.done; { --------------------------------------- list.done ---- }
var p : pnode;
begin
while beg <> nil do begin
p := beg;
dispose(p^.pm, done); { 1 }
beg := p^.next; { 2 }
dispose(p); { 3 }
end
end;
procedure report(message: string); { --------------------------- report ------- }
var s : string;
begin
str(MemAvail, s);
outtext(message + s);
moveto(0, GetY + 12);
end;
var stado : list;
x, y : integer;
gd, gm : integer;
p : pmonster;
i : word;
{ ---------------------------------- главная программа ------------------------ }
begin
gd := detect; initgraph(gd, gm, '...');
if graphresult <> grOk then begin
writeln('ошибка инициализации графики'); exit end;
randomize;
report(' доступно в начале программы: ');
stado.init;
for i := 1 to n do begin
case random(2) of
0 : p := new(pmonster, init(random(600), random(440), 10, 8));
1 : p := new(pdaemon, init(random(600), random(440), 10, 8, 6));
end;
stado.add(p); { добавление объекта в список }
end;
report(' доступно после выделения памяти: ');
stado.draw; { отрисовка объектов }
stado.done; { уничтожение объектов }
report(' доступно после освобождения памяти: ');
readln;
end.
Подробные пояснения к этой программе приведены в учебнике .
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.