Полиморфизм времени исполнения обеспечивается за счет использования производных классов и виртуальных функций. Виртуальная функция — это функция, объявленная с ключевым словом virtual в базовом классе и переопределенная в одном или в нескольких производных классах. Виртуальные функции являются особыми функциями, потому что при вызове объекта производного класса с помощью указателя или ссылки на него С++ определяет во время исполнения программы, какую функцию вызвать, основываясь на типе объекта. Для разных объектов вызываются разные версии одной и той же виртуальной функции. Класс, содержащий одну или более виртуальных функций, называется полиморфным классом (polymorphic class).
Виртуальная функция объявляется в базовом классе с использованием ключевого слова virtual. Когда же она переопределяется в производном классе, повторять ключевое слово virtual нет необходимости, хотя и в случае его повторного использования ошибки не возникнет.
В качестве первого примера виртуальной функции рассмотрим следующую короткую программу:
// небольшой пример использования виртуальных функций
#include <iostream.h>
class Base {
public:
virtual void who() { // определение виртуальной функции
cout << *Base\n";
}
};
class first_d: public Base {
public:
void who() { // определение who() применительно к first_d
cout << "First derivation\n";
}
};
class seconded: public Base {
public:
void who() { // определение who() применительно к second_d
cout << "Second derivation\n*";
}
};
int main()
{
Base base_obj;
Base *p;
first_d first_obj;
second_d second_obj;
p = &base_obj;
p->who(); // доступ к who класса Base
p = &first_obj;
p->who(); // доступ к who класса first_d
p = &second_ob;
p->who(); // доступ к who класса second_d
return 0;
}
Программа выдаст следующий результат:
Base
First derivation
Second derivation
Проанализируем подробно эту программу, чтобы понять, как она работает.
Как можно видеть, в объекте Base функция who() объявлена как виртуальная. Это означает, что эта функция может быть переопределена в производных классах. В каждом из классов first_d и second_d функция who() переопределена. В функции main() определены три переменные. Первой является объект base_obj, имеющий тип Base. После этого объявлен указатель р на класс Base, затем объекты first_obj и second_obj, относящиеся к двум производным классам. Далее указателю р присвоен адрес объекта base_objи вызвана функция who(). Поскольку эта функция объявлена как виртуальная, то С++ определяет на этапе исполнения, какую из версий функции who() употребить, в зависимости от того, на какой объект указывает указатель р. В данном случае им является объект типа Base, поэтому исполняется версия функции who(), объявленная в классе Base. Затем указателю р присвоен адрес объекта first_obj. (Как известно, указатель на базовый класс может быть использован для любого производного класса.) После того, как функция who() была вызвана, С++ снова анализирует тип объекта, на который указывает р, для того, чтобы определить версию функции who(), которую необходимо вызвать. Поскольку р указывает на объект типа first_d, то используется соответствующая версия функции who(). Аналогично, когда указателю р присвоен адрес объекта second_obj, то используется версия функции who(), объявленная в классе second_d.
Наиболее распространенным способом вызова виртуальной функции служит использование параметра функции. Например, рассмотрим следующую модификацию предыдущей программы:
/* Здесь ссылка на базовый класс используется для доступа к виртуальной функции */
#include <iostream.h>
class Base {
public:
virtual void who() { // определение виртуальной функции
cout << "Base\n";
}
};
class first_d: public Base {
public:
void who () { // определение who() применительно к first_d
cout << "First derivation\n";
}
};
class second_d: public Base {
public:
void who() { // определение who() применительно к second_d
cout << "Second derivation\n*";
}
};
// использование в качестве параметра ссылки на базовый класс
void show_who (Base &r) {
r.who();
}
int main()
{
Base base_obj;
first_d first_obj;
second_d second_obj;
show_who (base_ob j) ; // доступ к who класса Base
show_who(first_obj); // доступ к who класса first_d
show_who(second_obj); // доступ к who класса second_d
return 0;
}
Эта программа выводит на экран те же самые данные, что и предыдущая версия. В данном примере функция show_who() имеет параметр типа ссылки на класс Base. В функции main() вызов виртуальной функции осуществляется с использованием объектов типа Base, first_d и second_d. Вызываемая версия функции who() в функции show_who() определяется типом объекта, на который ссылается параметр при вызове функции.
Ключевым моментом в использовании виртуальной функции для обеспечения полиморфизма времени исполнения служит то, что используется указатель именно на базовый класс. Полиморфизм времени исполнения достигается только при вызове виртуальной функции с использованием указателя или ссылки на базовый класс. Однако ничто не мешает вызывать виртуальные функции, как и любые другие «нормальные» функции, однако достичь полиморфизма времени исполнения на этом пути не удается.
На первый взгляд переопределение виртуальной функции в производном классе выглядит как специальная форма перегрузки функции. Но это не так, и термин перегрузка функции не применим к переопределению виртуальной функции, поскольку между ними имеются существенные различия. Во-первых, функция должна соответствовать прототипу. Как известно, при перегрузке обычной функции число и тип параметров должны быть различными. Однако при переопределении виртуальной функции интерфейс функции должен в точности соответствовать прототипу. Если же такого соответствия нет, то такая функция просто рассматривается как перегруженная и она утрачивает свои виртуальные свойства. Кроме того, если отличается только тип возвращаемого значения, то выдается сообщение об ошибке. (Функции, отличающиеся только типом возвращаемого значения, порождают неопределенность.) Другим ограничением является то, что виртуальная функция должна быть членом, а не другом класса, для которого она определена. Тем не менее виртуальная функция может быть другом другого класса. Хотя деструктор может быть виртуальным, но конструктор виртуальным быть не может.
В силу различий между перегрузкой обычных функций и переопределением виртуальных функций будем использовать для последних термин переопределение (overriding).
Если функция была объявлена как виртуальная, то она и остается таковой вне зависимости от количества уровней в иерархии классов, через которые она прошла. Например, если класс second_d получен из класса first_d, а не из класса Base, то функция who() останется виртуальной и будет вызываться корректная ее версия, как показано в следующем примере:
// порождение от first_d, а не от Base
class second_d: public first_d {
public:
void who() { // определение who() применительно к second_d
cout << "Second derivation\n*";
}
};
Если в производном классе виртуальная функция не переопределяется, то тогда используется ее версия из базового класса. Например, запустим следующую версию предыдущей программы:
#include <iostream.h>
class Base {
public:
virtual void who() {
cout << "Base\n";
}
};
class first_d: public Base {
public:
void who() {
cout << "First derivation\n";
}
};
class second_d: public Base {
// who() не определяется
};
int main()
{
Base base_obj;
Base *p;
first_d first_obj; ,
second_d second_obj;
p = &base_obj;
p->who(); // доступ к who класса Base
p = &first obj;
p->who(); // доступ к who класса first_d
p = &sepond_ob;
p->who(); /* доступ к who() класса Base, поскольку second_d не переопределяет */
return 0;
}
Эта программа выдаст следующий результат:
Base
First derivation
Base
Надо иметь в виду, что характеристики наследования носят иерархический характер. Чтобы проиллюстрировать это, предположим, что в предыдущем примере класс second_d порожден от класса first_d вместо класса Base. Когда функцию who() вызывают, используя указатель на объект типа second_d (в котором функция who() не определялась), то будет вызвана версия функции who(), объявленная в классе first_d, поскольку этот класс — ближайший к классу second_d. В общем случае, когда класс не переопределяет виртуальную функцию, С++ использует первое из определений, которое он находит, идя от потомков к предкам.