Объектно-ориентированное программирование, страница 14

В языке С++ указатель на базовый класс может содержать адрес объекта производного класса. При этом нет необходимости использовать операцию явного приведения типов вида: pp = (Parent*)&dobj;. Итак, указатель pp содержит адрес объекта dobj производного класса. Туда же показывает и Derived *pd. Какие методы будут вызваны в каждом из последующих операторов программы?

pp->print();  pd->print();

Второй оператор не вызывает сомнений. Так как используется указатель на класс Derived, который содержит в данный момент адрес объекта dobj того же класса, то будет вызван метод Derived::print(). Действия для первого оператора не так очевидны. Полиморфизм раннего связывания диктует следующее правило. Так как pp объявлен указателем на класс Parent, то оператор pp->print(); вызовет Parent::print(). Тот факт, что pp в данный момент времени содержит адрес объекта производного класса не влияет на решение, принятое на этапе компиляции.

Теперь рассмотрим этот же пример, но с одним небольшим изменением. Добавим спецификатор virtual в объявление метода print в базовом классе. В этих условиях изменится работа только одного оператора: pp->print. В игру вступает полиморфизм позднего связывания и выбор метода print производится на этапе исполнения программы в зависимости от того, на объект какого класса ссылается в данный момент времени указатель Parent *pp. Если он ссылается на объект производного класса (как есть в нашем случае), то будет вызван Derived::print(), если же указатель ссылается на объект базового класса, то будет вызван Parent::print(). Такая гибкость виртуальных функций является мощным средством, позволяющим выбрать нужный метод при получении указателем (на текущий объект одного из производных классов иерархии) сообщения общего характера вида print(), input() и т. д.

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

Отметим, что симметричное присвоение pd=&pobj; не проходит, так как нарушена иерархия. Указатели на низшие классы не могут ссылаться на объекты высших классов. Возможно, однако, явное приведение типов, которое делает присвоение законным:

pd = (Derived*)&pobj;   // Явное приведение типа

После такого присвоения вызов pd->print() будет функционировать в зависимости от наличия описателя virtual в объявлении функции print в базовом классе. Если print виртуальная, то будет вызвана функция Parent::print(), если нет, то Derived::print().

Все методы классов могут быть виртуальными, за исключением конструктора. Деструктор класса может быть виртуальным. Это позволяет вызвать требуемый деструктор с помощью указателя на базовый класс. Если указатель в данный момент ссылается на объект одного из производных классов, то будет вызван деструктор соответствующего класса. Деструктор класса производного от класса с virtual деструктором сам является virtual деструктором (так же как и любая другая виртуальная функция). Например,

class Box

{

public:

virtual ~Box() { puts("Box"); } // Виртуальный деструктор

};

class Menu : public Box

{

public:

~Menu() { puts("Menu"); } // ~Menu() тоже виртуальный

};

class PullDownMenu : public Menu

{

public:

~PullDownMenu() { puts("Pull"); } // Этот деструктор - тоже виртуальный

};

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

Box *window[3];

window[0] = new Box;

window[1] = new Menu;

window[2] = new PullDownMenu;

Вызовы деструкторов порождают следующие цепочки действий:

delete window[0];    // Будет вызван ~Box()

delete window[1];    // Будет вызван ~Menu(), потом ~Box()

delete window[2];    // Будет вызван ~PullDownMenu(), потом ~Menu(), и наконец ~Box()

Если убрать спецификатор virtual при объявлении деструктора ~Box(), то во всех трех случаях будет вызван деструктор базового класса, который возможно некорректно сработает в последних двух случаях при уничтожении объектов классов Menu и PullDownMenu.

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

В случае перекрытия (hiding), количество и типы аргументов не влияют на факт перекрытия. Совмещение (overloading) функций с одним именем, но различием в аргументах возможно только в рамках одного класса и одинаковом типе возвращаемого значения. Если объявленная виртуальной в базовом классе функция имеет в производном классе другой тип или набор параметров, то она считается спрятанной (hiden) и виртуальный механизм игнорируется. Если в производном классе она имеет другой тип возвращаемого значения, то это рассматривается как ошибка. Все рассмотренные случаи можно проиллюстрировать следующим схематичным примером:

class A { };           // Базовый класс

class B : public A { };// Производный класс

class C                // Базовый класс (другая иерархия)

{

public:

void F();             // Обычный метод класса