Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ТП лекции Раздел 4.doc
Скачиваний:
16
Добавлен:
28.09.2019
Размер:
2.56 Mб
Скачать

4.9.1. Полиморфизм раннего связывания.

Полиморфизм наряду с инкапсуляцией, переопределением методов и операций, скрытием и наследованием данных является еще одним фундаментальным поня­тием в ООП. Термин «полиморфизм» происходит от латинских корней, которые можно перевести как многообразие форм или множественность форм. Его реализация в C++ позволяет пользователю иерархической структуры классов посылать сообщения общего характера объектам разных классов, не заботясь о деталях ин­терпретации конкретного действия внутри класса. Одно и то же по смыслу дей­ствие может иметь множество конкретных воплощений, форм реализации. Поли­морфизм сильно облегчает задачу пользования библиотекой классов. Пользователь должен понимать лишь общий смысл или эффект воздействия метода на любой объект в иерархии классов, а не особенности его реализации для конкретного класса.

Главным моментом в понимании сути полиморфизма является момент време­ни, в который система определяет детали реализации метода для объекта. Если система решает, как выполнить действие во время компиляции, то этот процесс называется ранним связыванием (early binding), если решение принимается ди­намически во время исполнения программы, то это называется поздним связыва­нием (late binding). Под связыванием понимают связывание сути послания (име­ни метода) с конкретными кодами его реализации для данного объекта.

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

4.9.2. Полиморфизм позднего связывания и виртуальные функции.

Виртуальные функции являются средством, с помощью которого полиморфизм позднего связывания осуществляется на практике. Если в базовом классе Parent определить какую-нибудь функцию (метод) как virtual, то она может быть пе­реопределена (overriden) в одном (или более) из производных классов. Преиму­щества такого определения функций в родственных классах будут ясны после того, как мы вспомним, что дает переопределение обычной, не виртуальной функ­ции в производном классе Derived. Пусть для определенности в обоих классах была объявлена public-функция void print(). Рассмотрим теперь фрагмент функ­ции main, использующий объекты классов Parent и Derived:

Parent *pp,pobj;

Derived *pd,dobj;

pobj.print (); // Будут вызваны разные printQ

dobj.print(); // Полиморфизм раннего связывания

Компилятор заранее решает, каким классам должны принадлежать функции print в последних двух операторах. Очевидно, что для объекта pobj класса Parent будет вызван метод своего же класса, то есть Parent::print(). Аналогично для производного класса справедлив вызов Derived::print().

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

pp->print(): pd->print():

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

Теперь рассмотрим этот же пример, но с одним небольшим изменением. Доба­вим спецификатор 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() тоже виртуальный

~Menu() { puts(“Menu”); }

}

class PullDownMenu : public Menu

{ publiс: // Этот деструктор тоже виртуальный

~PullDownMenu() { puts(“Pull”); }

}

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

Box *window[3];

window[0] = new Box;

window[l] = new Menu;

window[2] = new PullDownMenu;

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

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

delete window[l]; // Будет вызван ~Мепи(), потом ~Вох()

delete window[2]; // Будет вызван ~PullDownMenu(),

// потом ~Мепи(), и, наконец, ~Вох()

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

При определении в производных классах виртуальных функций количество и типы параметров у всех функций в разных классах иерархии должны быть одина­ковы. Только в этом случае они считаются виртуальными.

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

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