Полиморфизм

advertisement
Полиморфизм

Полиморфизм (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<3; count++) b_ptr[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

Чтобы использовать механизм виртуальных функций, вы должны
применять указатели или ссылки
Download