Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Методичка ООП.doc
Скачиваний:
23
Добавлен:
08.11.2018
Размер:
1.4 Mб
Скачать

Статическое перекрытие виртуальных методов

Рассмотрим далее следующую ситуацию: в классе-предке объявлен виртуальный метод, а в классе-потомке – метод с теми же именем и типом, но при его объявлении пропущена директива override. В этом случае произойдет статическое перекрытие и при вызове этого метода через указатель на класс-предок всегда будет вызываться метод предка и полиморфизма не будет. Например:

interface Type TSomeObject1 = class(TObject) procedure SomeMethod; virtual; end;

TSomeObject2 = class(TSomeObject1) //метод SomeMethod был объявлен как виртуальный, но перекрыт //как статический procedure SomeMethod; end;

Implementation procedure Test; Var SomeObject: TSomeObject1; begin SomeObject := TSomeObject2.Create; SomeObject.SomeMethod; //будет вызван TSomeObject1.SomeMethod end;

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

Это удобно, если для части классов-потомков необходимо статическое перекрытие методов, а для части виртуальное или динамическое. Но возможно программист опустил директиву override по ошибке. Поэтому компилятор выдаст предупреждение о скрытии ранее определенного виртуального метода. Чтобы убрать это предупреждение в случае, если нужно именно статическое перекрытие, используется директива reintroduce:

interface Type TSomeObject1 = class(TObject) procedure SomeMethod; virtual; end;

TSomeObject2 = class(TSomeObject1) //метод SomeMethod был объявлен как виртуальный, но перекрыт //как статический, что явно показывает директива reintroduce procedure SomeMethod; reintroduce; end;

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

Виртуальное перекрытие конструкторов и деструкторов

Вернемся к рассмотрению конструкторов и деструкторов – могут ли они быть виртуальными. Рассмотрим сначала деструкторы: деструктор может быть объявлен как виртуальный или динамический. Это позволяет по указателю на объект базового класса вызвать необходимый деструктор в случае, когда указатель фактически ссылается на объект производного класса.

TDat = class ... destructor Destroy; virtual; end;

TDat1 = class(TDat) Destructor Destroy; override; end;

Procedure var a: array[1..20] of TDat; i: integer; begin //создадим объекты разных типов //по четным номерам – TDat, по нечетным – TDat1 for i := 1 to 20 do if i mod 2 = 0 then a[i] := TDat.Create else a[i] := TDat1.Create ...

for i := 1 to 20 do a[i].Destroy; //будет вызван деструктор класса реально созданного объекта end;

В этом примере будут вызываться деструкторы, соответствующие классу реально созданного объекта, то есть класса TDat для четных элементов массива и TDat1 для нечетных. Реализации этих деструкторов, возможно, вызывают деструкторы предков, используя директиву inherited.

При попытке откомпилировать такую программу компилятор выдаст предупреждение о том, что метод Destroy класса TDat скрывает метод Destroy класса-предка. Это вызвано тем, что в Delphi все классы являются потомками класса TObject, у которого определен деструктор Destroy, объявленный виртуальным. И если новому классу нужен собственный деструктор, то он должен перекрывать деструктор класса TObject, т.е. быть объявлен как override:

TDat = class ... destructor Destroy; override; //перекрывает деструктор класса TObject end;

TDat1 = class(TDat) Destructor Destroy; override; //перекрывает деструктор класса TDat end;

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

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

Дополним рассмотренный выше пример иерархии классов графических объектов виртуальными деструкторами:

interface type TRectangle = class procedure Draw; virtual; //рисование procedure Erase; virtual; //стирание destructor Destroy; override; ... end;

TPicture = class(TRectangle) procedure Draw; override; //рисование procedure Erase; override; //стирание destructor Destroy; override; ... end;

TText = class(TRectangle) procedure Draw; override; //рисование procedure Erase; override; //стирание destructor Destroy; override; ... end;

Implementation destructor TRectangle.Destroy; begin //стираем прямоугольник Erase; //Вызываем деструктор предка inherited; end;

destructor TPicture.Destroy; begin //стираем картинку Erase; //Вызываем деструктор предка inherited; end;

destructor TText.Destroy; begin //стираем текст Erase; //Вызываем деструктор предка inherited; end;

Procedure Test; var Figure: TRectagle; begin Figure := TRectangle.Create; Figure.Draw; //будет вызван TRectangle.Draw Figure.Destroy; //будет вызван деструктор Trectangle

Figure := TPicture.Create; Figure.Draw; //будет вызван TPicture.Draw Figure.Destroy; //будет вызван деструктор Tpicture

Figure := TText.Create; Figure.Draw; //будет вызван TText.Draw Figure.Destroy; //будет вызван деструктор TText end;

Если сравнить реализацию деструкторов всех этих классов, видно, что они выполняют однотипные действия – вызывают виртуальный метод стирания Erase и деструктор предка. При этом реально выполнятся разные действия, поскольку методы Erase по своим реализациям различаются. Если вспомнить, что при вызове виртуального или динамического метода из метода, определенного в классе-предке, вызовется метод, соответствующий реальному классу объекта, то можно переписать этот пример следующим образом:

interface type TRectangle = class procedure Draw; virtual; //рисование procedure Erase; virtual; //стирание destructor Destroy; override; ... end;

TPicture = class(TRectangle) procedure Draw; override; //рисование procedure Erase; override; //стирание //деструктор не определен ... end;

TText = class(TRectangle) procedure Draw; override; //рисование procedure Erase; override; //стирание //деструктор не определен ... end;

Implementation destructor TRectangle.Destroy; begin //Вызываем виртуальный метод стирания фигуры Erase; //Вызываем деструктор предка inherited; end;

В этом варианте у классов-потомков TText и TPicture не определены деструкторы. В этом случае, как известно, компилятор автоматически создает виртуальный деструктор, который не выполнит никаких дополнительных действий, кроме вызова деструктора предка. То есть при уничтожении объектов типа TText и TPicture вызовутся неявно созданные компилятором виртуальные деструкторы. Эти деструкторы перед тем, как уничтожить объекты, в свою очередь, вызовут деструктор предка TRectangle. В этом деструкторе вызовется тот метод стирания Erase, который соответствует реальному типу объекта. И произойдет стирание либо прямоугольника, либо рисунка, либо рамки текста. Что и требовалось.

При вызове конструктора мы указываем не имя объекта, а имя класса. То есть именно при создании объекта и выясняется, какого он будет типа. Таким образом, в общем случае конструктор класса не может быть виртуальным. При описании конструктора можно использовать директивы virtual, dynamic, override. Это никак не повлияет на процесс создания объектов. Если вызывать конструктор как обычный метод – например для повторной инициализации – то виртуальное перекрытие конструкторов учитывается так же, как и для обычных методов. Кроме этого возможно использование виртуальных конструкторов при работе с метаклассами (указателями на класс). Механизм указателей на класс рассматривается позднее.

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

interface type //Класс – абстрактная фигура TFigure = class(TObject) private C: TColor; public //метод возвращает текущий цвет – общий у всех фигур Function GetColor: TColor; //не перекрывается

//метод отрисовки фигуры – свой у каждого типа фигур, procedure Draw; virtual; //перекрывается как виртуальный

//метод обработки щел­чка мыши с координатами x,y, //возвращает True, если координаты мыши находят­ся внутри //этой фигуры function MouseClick(x,y: integer): boolean; virtual; //перекрывается как виртуальный end;

//Класс - окружность TCircle = class(TFigure) private //круг задается координатами центра и радиусом Fx,Fy,FR: integer; public //конструктор инициализации constructor Create(x,y,R: integer); //виртуальный деструктор – перекрывается деструктор TObject destructor Destroy; override; //перекрывается метод отрисовки фигуры procedure Draw; override; //перекрывается метод обработки щел­чка мыши function MouseClick(x,y: integer): boolean; override; end;

//Класс - эллипс TEllipse = class(TFigure) private //эллипс задается координатами центра и полуосями Fx,Fy,FA,FB: integer; public //конструктор инициализации constructor Create(x,y,A,B: integer); //виртуальный деструктор destructor Destroy; override; //перекрывается метод отрисовки фигуры procedure Draw; override; //перекрывается метод обработки щел­чка мыши function MouseClick(x,y: integer): boolean; override; end;

//Класс - квадрат TSquare = class(TFigure) private //квадрат задается координатами угла и длиной стороны Fx,Fy,Fl: integer; public //конструктор инициализации constructor Create(x,y,l: integer); //виртуальный деструктор destructor Destroy; override; //перекрывается метод отрисовки фигуры procedure Draw; override; //перекрывается метод обработки щел­чка мыши function MouseClick(x,y: integer): boolean; override; end;

//Класс - прямоугольник TRectangle = class(TFigure) private //прямоугольник задается координатами двух углов Fxl,Fyl,Fxr,Fyr: integer; public //конструктор инициализации constructor Create(xl,yl,xr,yr: integer); //виртуальный деструктор destructor Destroy; override; //перекрывается метод отрисовки фигуры procedure Draw; override; //перекрывается метод обработки щел­чка мыши function MouseClick(x,y: integer): boolean; override; end;

//Класс – схема фигур TScheme = class(TObject) private // число фигур схемы n: integer; //массив указателей на фигуры схемы – в соответствие с правилом //совместимости типов может хранить указатели на объект //любого класса, производного от TFigure Fig: array[1..100] of TFigure; public //Метод добавления фигуры procedure AddFig(f: TFigure); // Метод обработки щелчка мыши procedure ScMouseClick(x,y: integer); // Метод отображения схемы procedure ScDraw; constructor Create; destructor Destroy; override; end;

implementation constructor TScheme.Create; begin inherited Create; n := 0; //конструктор можно было не определять end;

destructor TScheme.Destroy; var i: integer; begin //В цикле уничтожаются все фигуры схемы – будут вызываться //деструкторы, соответствующие реальным типам объектов for i := 1 to n do Fig[i].Destroy; inherited Destroy; end;

procedure TScheme.AddFig(f: TFigure); begin //В схему добавляется фигура, в качестве параметра процедуры //реально может передаваться объект любого класса, //производного от TFigure n:=n+1; Fig[n]:=f; end;

procedure TScheme.ScDraw; var i: integer; begin //будет вызван метод Draw соответствующий реальному //классу объекта for i:=1 to n do Fig[i].Draw; end;

procedure TScheme.ScMouseClick(x, y: integer); var i: integer; begin i := 1; //будет вызван метод MouseClick соответствующий реальному //классу объекта while (i<=n) and (not Fig[i].MouseClick(x,y)) do i:=i+1; end;

В этом примере метод Draw для иерархии классов фигур объявлен виртуальным, то есть из метода TScheme.ScDraw будет вызываться метод соответствующий реальному классу объекта. И изображаться, соответственно, будет не абстрактная фигура, а круг, эллипс, прямоугольник или квадрат в зависимости от того какие объекты были добавлены в схему с помощью метода AddFig. Аналогично метод MouseClick, вызываемый из TScheme.ScMouseClick будет определять, щелкнули ли в пределах конкретной фигуры.

Рассмотрим использование этих классов:

//Глобальная переменная Var Scheme: TScheme;

//Метод интерфейсного класса TForm1 в котором создаем схему procedure TForm1.FormCreate(Sender: TObject); var f: TFigure; begin //Для примера создадим схему, состоящей из одного круга, //одного эллипса, одного квадрата и одного прямоугольника Scheme := TScheme.Create; f := TCircle.Create(10,10,3); Scheme.AddFig(f); f := TEllipse.Create(20,10,3,5); Scheme.AddFig(f); f := TSquare.Create(30,20,10); Scheme.AddFig(f); f := TRectangle.Create(50,50,60,70); Scheme.AddFig(f); Scheme.ScDraw; //Метод TScheme.ScDraw вызовет последовательно методы Draw //классов TCircle, TEllipse, TSquare, TRectangle для объектов //соответствующих классов end;

//Метод интерфейсного класса TForm1 в котором уничтожаем схему procedure TForm1.FormDestroy(Sender: TObject); begin Scheme.Destroy; //Деструктор TScheme вызовет последовательно деструкторы //созданных объектов классов TCircle, TEllipse, TSquare, //TRectangle – в зависимости от реального типа объекта end;

//Метод интерфейсного класса TForm1, который обрабатывает //щелчок кнопки мыши в точке с координатами X,Y: integer procedure TForm1.FormMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: integer); Begin Scheme.ScMouseClick(x,y); //Метод TScheme.ScMouseClick вызовет последовательно метод //MouseClick для созданных объектов классов TCircle, TEllipse, //TSquare, TRectangle – вызываться будет метод класса, //соответствующего реальному типу объекта end;

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

    1. Полиморфизм в C++

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

Функция объявляется виртуальной в базовом классе с помощью ключевого слова virtual перед объявлением функции:

сlass CBase { ... public: virtual int funct1(void);//объявление виртуальной функции ... };

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

class CBase //базовый класс { public: //виртуальные функции virtual void vf1(); virtual void vf2(); virtual void vf3(); virtual void vf4(); //не виртуальная функция void f(); };

class CDerived : public CBase //производный класс { public: void vf1(); //перекрыли как виртуальную virtual void vf2();//перекрыли как виртуальную //спецификатор virtual допустим, но излишен void vf3(int); //статическое перекрытие, поскольку //другой набор параметров

//char vf4(); //недопустимо, изменен только //тип возвращаемого значения!

void f(); //статическое перекрытие };

//использование классов void extf()  { CBase * d; //указатель на базовый класс d = new CDerived(); //экземпляр производного класса

d–>vf1();  //будет вызван CDerived::vf1 d–>vf2();  //будет вызван CDerived::vf2 d–>vf3();  //будет вызван CBase::vf3  d–>f();    //будет вызван CBase::f т.к. он не виртуальный

delete d; }

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

При вызове функции с использованием оператора доступа к области действия «::» виртуальный механизм гарантированно не используется. То есть будет вызван метод именно того класса, который указан. Это позволяет, например, вызвать перекрытый метод предка из метода потомка. В противном случае такой вызов привел бы к рекурсии. Например:

class CBase //базовый класс { void DoSomething1(); void DoSomething2(); public: //виртуальная функция virtual void SomeMethod(); };

class CDerived : public CBase //производный класс { void DoSomething3(); public: void SomeMethod(); };

CBase::SomeMethod() //метод базового класса { DoSomething1(); DoSomething2(); }

CDerived::SomeMethod() //метод производного класса { CBase::SomeMethod();//будет вызван метод базового класса DoSomething3(); }

Конструкторы классов в языке C++ не могут быть виртуальными:

class CBase { ... public: //virtual CBase(); //не допустимо };

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

class CSomething1 //базовый класс { ... public: virtual ~CSomething1();//виртуальный деструктор ... }; class CSomething2 : public CSomething1//производный класс { ... public: ~СSomething2();//деструктор тоже будет виртуальным ... };

//использование классов void extf()  { CSomething1 * d[10]; //вектор указателей на базовый класс for(int i=0;i<10;i++) if (SomeCondition(i))//если выполняется некое условие //создается экземпляр базового класса d[i] = new CSomething1(); else //иначе //создается экземпляр базового класса d[i] = new CSomething2();

//использование объектов ...

//удаление for(int i=0;i<10;i++) delete d[i]; //будет вызван деструктор, //соответствующий реальному типу созданного объекта }

Как уже было сказано, деструкторы всех базовых классов будут вызваны из деструктора производного класса автоматически.