Программирование на языке высокого уровня Паскаль

Иерархии объектов. Работа с объектами в динамической памяти

Разбить на страницы
Показывать лекцию целиком

Наследование

Презентацию к данной работе Вы можете скачать здесь.

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

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

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

Объект в Паскале может иметь произвольное количество потомков и только одного предка. При описании объекта имя его предка записывается в круглых скобках после ключевого слова 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 для перемещения монстра: необходимо стереть его изображение на старом месте, обновить координаты и нарисовать на новом месте. На первый взгляд, можно без проблем унаследовать этот метод, а также метод hit. Так мы и поступим.

Добавим описание объекта 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.hit;

Результаты запуска программы разочаровывают: на экране появляется изображение не демона, а монстра — символ @! Значит, из метода move вызываются методы рисования и стирания объекта-предка. Да и метод атаки, вызываемый из hit, судя по диагностическому сообщению, также принадлежит объекту 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 в переводе с английского значит "фактический". Объявление метода виртуальным означает, что все ссылки на этот метод будут разрешаться по факту его вызова, то есть не на стадии компиляции, а во время выполнения программы. Этот механизм называется поздним связыванием. Для его реализации необходимо, чтобы адреса виртуальных методов хранились там, где ими можно будет в любой момент воспользоваться, поэтому компилятор формирует для этих методов таблицу виртуальных методов (VMT — virtual method table). В первое поле этой таблицы записывается размер объекта, а затем идут адреса виртуальных методов (в том числе и унаследованных) в порядке описания в объекте. Для каждого объектного типа создается одна VMT.

    Каждый объект во время выполнения программы должен иметь доступ к VMT. Обеспечение этой связи нельзя поручить компилятору, так как она должна устанавливаться позже — при создании объекта во время выполнения программы. Поэтому связь экземпляра объекта с VMT устанавливается с помощью специального метода, называемого конструктором.

    Класс, имеющий хотя бы один виртуальный метод, должен содержать конструктор:

    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 компилятор вставляет в начало метода фрагмент, который записывает ссылку на VMT в специальное поле объекта (память под это поле выделяется компилятором). Следовательно, прежде чем использовать виртуальные методы, необходимо вызвать конструктор объекта.

    Конструктор обычно используется для инициализации объекта. В нем выполняется выделение памяти под динамические переменные или структуры, если они есть в объекте, и присваиваются начальные значения. Если в объекте есть поля, которые также являются объектами, в конструкторе вызываются конструкторы этих объектов.

    Объект может содержать несколько конструкторов. Повторный вызов конструктора вреда программе не наносит, а вот если конструктор вообще не вызвать и попытаться использовать виртуальный метод, поведение программы не определено. Конструктор должен быть вызван для каждого создаваемого объекта. Присваивание одного объекта другому возможно только после конструирования обоих.

    Вызов виртуального метода выполняется так: из объекта берется адрес его VMT, из VMT выбирается адрес метода, а затем управление передается этому методу (рис 7.2). Таким образом, при использовании виртуальных методов из всех одноименных методов иерархии всегда выбирается тот, который соответствует фактическому типу вызвавшего его объекта.

    (рис 7.2) Позднее связывание

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

    Правила описания виртуальных методов:

  • Если в объекте метод определен как виртуальный, во всех потомках он также должен быть виртуальным. Отсутствие ключевого слова 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 и hit, унаследованных из базового класса, вызываются методы attack, draw и erase, определенные в производном классе.

    Виртуальные методы незаменимы и при передаче объектов в подпрограммы. В заголовке подпрограммы описывается либо объект базового типа, передаваемый по адресу, либо указатель на этот объект, а при вызове в нее передается объект или указатель производного класса. В этом случае виртуальные методы, вызываемые для объекта из подпрограммы, будут соответствовать типу аргумента, а не параметра.

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

    Объекты в динамической памяти

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

    Для выделения памяти под объекты используются процедура и функция 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);

    Для простых объектов деструктор может быть пустым, а для объектов, содержащих динамические поля, в нем записываются операторы освобождения памяти для этих полей. В деструкторе можно описывать любые действия, необходимые для конкретного объекта, например закрытие файлов. Исполняемый код деструктора никогда не бывает пустым, потому что компилятор по служебному слову destructor вставляет в конец тела метода операторы получения размера объекта из VMT. Деструктор передает этот размер процедуре 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 для перемещения монстра: необходимо стереть его изображение на старом месте, обновить координаты и нарисовать на новом месте. На первый взгляд, можно без проблем унаследовать этот метод, а также метод hit. Так мы и поступим.

    Добавим описание объекта 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.hit;

    Результаты запуска программы разочаровывают: на экране появляется изображение не демона, а монстра — символ @! Значит, из метода move вызываются методы рисования и стирания объекта-предка. Да и метод атаки, вызываемый из hit, судя по диагностическому сообщению, также принадлежит объекту 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 в переводе с английского значит "фактический". Объявление метода виртуальным означает, что все ссылки на этот метод будут разрешаться по факту его вызова, то есть не на стадии компиляции, а во время выполнения программы. Этот механизм называется поздним связыванием. Для его реализации необходимо, чтобы адреса виртуальных методов хранились там, где ими можно будет в любой момент воспользоваться, поэтому компилятор формирует для этих методов таблицу виртуальных методов (VMT — virtual method table). В первое поле этой таблицы записывается размер объекта, а затем идут адреса виртуальных методов (в том числе и унаследованных) в порядке описания в объекте. Для каждого объектного типа создается одна VMT.

    Каждый объект во время выполнения программы должен иметь доступ к VMT. Обеспечение этой связи нельзя поручить компилятору, так как она должна устанавливаться позже — при создании объекта во время выполнения программы. Поэтому связь экземпляра объекта с VMT устанавливается с помощью специального метода, называемого конструктором.

    Класс, имеющий хотя бы один виртуальный метод, должен содержать конструктор:

    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 компилятор вставляет в начало метода фрагмент, который записывает ссылку на VMT в специальное поле объекта (память под это поле выделяется компилятором). Следовательно, прежде чем использовать виртуальные методы, необходимо вызвать конструктор объекта.

    Конструктор обычно используется для инициализации объекта. В нем выполняется выделение памяти под динамические переменные или структуры, если они есть в объекте, и присваиваются начальные значения. Если в объекте есть поля, которые также являются объектами, в конструкторе вызываются конструкторы этих объектов.

    Объект может содержать несколько конструкторов. Повторный вызов конструктора вреда программе не наносит, а вот если конструктор вообще не вызвать и попытаться использовать виртуальный метод, поведение программы не определено. Конструктор должен быть вызван для каждого создаваемого объекта. Присваивание одного объекта другому возможно только после конструирования обоих.

    Вызов виртуального метода выполняется так: из объекта берется адрес его VMT, из VMT выбирается адрес метода, а затем управление передается этому методу (рис 7.2). Таким образом, при использовании виртуальных методов из всех одноименных методов иерархии всегда выбирается тот, который соответствует фактическому типу вызвавшего его объекта.

    (рис 7.2) Позднее связывание

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

    Правила описания виртуальных методов:

  • Если в объекте метод определен как виртуальный, во всех потомках он также должен быть виртуальным. Отсутствие ключевого слова 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 и hit, унаследованных из базового класса, вызываются методы attack, draw и erase, определенные в производном классе.

    Виртуальные методы незаменимы и при передаче объектов в подпрограммы. В заголовке подпрограммы описывается либо объект базового типа, передаваемый по адресу, либо указатель на этот объект, а при вызове в нее передается объект или указатель производного класса. В этом случае виртуальные методы, вызываемые для объекта из подпрограммы, будут соответствовать типу аргумента, а не параметра.

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

    Объекты в динамической памяти

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

    Для выделения памяти под объекты используются процедура и функция 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);

    Для простых объектов деструктор может быть пустым, а для объектов, содержащих динамические поля, в нем записываются операторы освобождения памяти для этих полей. В деструкторе можно описывать любые действия, необходимые для конкретного объекта, например закрытие файлов. Исполняемый код деструктора никогда не бывает пустым, потому что компилятор по служебному слову destructor вставляет в конец тела метода операторы получения размера объекта из VMT. Деструктор передает этот размер процедуре 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.

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

    Вернуться к учебному плану