Полиморфизм Полиморфизм (polymorphism) - последний из трех "китов", на которых держится объектно-ориентированное программирование Слово это можно перевести с греческого как "многоформенность Применительно с С++ этот термин обычно трактуют как способность объекта отреагировать на некоторый запрос (т.е. вызвать функцию-член) сообразно своему типу, даже если на стадии компиляции тип объекта, к которому направлен запрос, еще неизвестен В С++ полиморфизм реализуется при помощи механизмов виртуальных функций членов
Виртуальные функции-члены К механизму виртуальных функций обращаются в тех случаях, когда в базовый класс необходимо поместить функцию, которая должна по разному выполняться в производных классах. Точнее, по разному должна выполняться не единственная функция из базового набора, а в каждом производном классе требуется свой вариант этой функции
Eсли используется класс, у которого есть производные и базовые классы, имеющие функции с одним именем и одинаковым набором аргументов, компилятор не в состоянии определить, к какому конкретно классу относится указываемый объект и какую функцию для него вызывать class Base { void f(); }; class Deriv1: public Base { void f(); }; class Deriv2: public Base { void f(); }; class Deriv3: public Deriv2 { void f(); }; // Base *b_ptr[3]; *b_ptr[0]=new Deriv1; *b_ptr[1]=new Deriv2; *b_ptr[2]=new Deriv3; //... for (int count=0; count f(); // для всех объектов будет вызвана Base::f()
Термин позднее связывание (late binding) значит, что компилятор не может определить заранее, какая функция должна вызываться на самом деле, если обращение к виртуальной функции использует указатель или ссылку на базовый класс Хотя переменная и определяется как указатель на базовый класс, она в действительности может указывать на объект производного класса Эта особенность виртуального механизма приводит к тому, что адрес функции может быть найден только во время исполнения программы При вызове функции-члена при помощи указателя необходимо решить проблему распознавания класса, к которому принадлежит указываемый объект, для того что бы функция корректно выполнилась
Возможны три решения: Первое: обеспечить, чтобы всегда указывались только объекты одного типа. Это, конечно, радикальное решение, но оно вносит существенные ограничения Второе: поместить в базовый класс поле типа, которое смогут просматривать функции. Это решение уже несколько лучше, но только в том случае, если как сам базовый класс, так и производные от него классы, создает и использует один программист в пределах небольших программ
enum CLASS_ID {ID_base, ID_deriv1, ID_deriv2}; class Base { protected: CLACC_ID class_ID; public: Base() { class_ID=ID_base; /*... */ } void f(); }; class Deriv1: public Base { //... Deriv1(){ class_ID=ID_deriv1; /*... */ } friend void Base::f(); }; class Deriv2: public Base { //... int some_member; Deriv2() { class_ID=ID_deriv2; /*... */ } friend void Base::f(); };
void Base::f() { switch (class_ID) { case ID_Base: // Операторы break; case ID_Deriv1: // break; case ID_Deriv2: // break; default: // Обработка неизвестного номера класса } Программа весьма запутанна Если программист пожелает добавить новый класс, придется модифицировать исходный текст f() функции-члена класса Base
Третье: использование виртуальных функций - фактически перекладывает на компилятор все заботы о помещении в класс поля типа, генерации кода для его проверки (во время выполнения программы!) и вызова функции-члена, соответствующей реальному типу объекта Ключевое слово virtual предписывает компилятору генерировать некоторую дополнительную информацию о функции Если в некотором классе имеется функция, описанная как virtual, то в такой класс компилятором добавляется скрытый член - указатель на таблицу виртуальных функций, а также генерируется специальный код, позволяющий осуществить выбор виртуальной функции во время работы программы Конечно, использование виртуальных функций снижает быстродействие программы и увеличивает размер
Виртуальные функции позволяют программисту описывать в базовом классе функции, которые можно переопределять в любом производном классе. Компилятор и загрузчик обеспечивают правильное соответствие между объектами и применяемыми к ним функциями Например:
// Базовый класс с виртуальной и не виртуальной функцией class Base { public: virtual void virt() { printf("Hello from Base::virt\n");} void nonVirt() { printf("Hello from Base::nonVirt\n");} }; // Производный класс заменяет обе функции class Derived : public Base { public: void virt() { printf("Hello from Derived::virt\n");} void nonVirt() { printf("Hello from Derived::nonVirt\n");} };
int main(void) { Base *bp = new Derived;// базовый указатель // реально ссылающийся // на производный объект bp->virt(); bp->nonVirt(); return 0; } Результат: Hello from Derived::virt Hello from Base::nonVirt
Для виртуальных функций существуют следующие правила: виртуальную функцию нельзя объявить как static спецификатор virtual необязателен при переопределении функции в производном классе виртуальная функция должна быть определена, либо должна описываться как чистая Обойти виртуальный механизм можно так bp->Base::virt(); Hello from Base::virt
В базовых классах необходимо использовать виртуальные деструкторы. Рассмотрим ситуацию: class Base { public: ~Base() { // Освобождение ресурсов } }; class Derived : public Base { public: ~Derived() { // Освобождение ресурсов } }; int main(void) { Base *bp = new Derived; // delete bp; }
Так как деструктор не является виртуальным, delete вызовет только Base::~Base, что может привести к потере ресурсов Derived Чтобы использовать механизм виртуальных функций, вы должны применять указатели или ссылки