Теперь можно разобраться, почему у виртуальных функций должен быть одинаковый список параметров в базовом и производном классах (см. раздел 15.3). Если функции-члены в базовом и производном классах будут получать разные аргументы, не будет никакого способа вызвать версию производного класса через ссылку или указатель на базовый. Например:
class Base {
public:
virtual int fcn();
};
class D1 : public Base {
public:
//
//
int fcn(int); //
virtual void f2(); //
//
};
class D2 : public D1 {
public:
int fcn(int); //
int fcn(); //
void f2(); //
};
Функция fcn()
в классе D1
не переопределяет виртуальную функцию fcn()
из класса Base
, поскольку у них разные списки параметров. Вместо этого она fcn()
из базового класса. Фактически у класса D1
есть две функции по имени fcn()
: класс D1
унаследовал виртуальную функцию fcn()
от класса Base, а также определяет собственную невиртуальную функцию-член по имени fcn()
, получающую параметр типа int
.
С учетом классов, описанных выше, рассмотрим несколько разных способов вызова этих функций:
Base bobj; D1 d1obj; D2 d2obj;
Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj
bp1->fcn(); //
bp2->fcn(); //
bp3->fcn(); //
D1 *d1p = &d1obj D2 *d2p = &d2obj
bp2->f2(); //
d1p->f2(); //
d2p->f2(); //
Все три первых вызова сделаны через указатели на базовый класс. Поскольку функция fcn()
является виртуальной, компилятор создает код, способный во время выполнения решить, какую версию вызвать.
Это решение будет принято на основании фактического типа объекта, с которым связан указатель. В случае указателя bp2
основной объект имеет тип D1
. Этот класс не переопределит функцию fcn()
без параметров. Таким образом, вызов через указатель bp2
распознается (во время выполнения) как версия, определенная в классе Base
.
Следующие три вызова осуществляются через указатели с отличными типами. Каждый указатель указывает на один из типов в этой иерархии. Первый вызов некорректен, так как в классе Base
нет функции f2()
. Тот факт, что указатель случайно указывает на производный объект, является несущественным.
И наконец, рассмотрим вызовы невиртуальной функции fcn(int)
:
Base *p1 = &d2obj D1 *p2 = &d2obj D2 *p3 = &d2obj
p1->fcn(42); //
p2->fcn(42); //
p3->fcn(42); //
В каждом вызове указатель случайно указывает на объект типа D2
. Но динамический тип не имеет значения, когда происходит вызов невиртуальной функции. Вызываемая версия зависит только от статического типа указателя.
Подобно любой другой функции, функция-член (виртуальная или нет) может быть перегружена. Производный класс способен переопределить любое количество экземпляров перегруженных функций, которые он унаследовал. Если производный класс желает сделать все перегруженные версии доступными через свой тип, то он должен переопределить их все или ни одну из них.