На этом шаге мы подведем итоги по использованию виртуальных функций.
Виртуальными могут быть не любые функции, а только нестатические компонентные функции какого-либо класса. После того как функция определена как виртуальная, ее повторное определение в производном классе (с тем же самым прототипом) создает в этом классе новую виртуальную функцию, причем спецификатор virtual может не использоваться.
В производном классе нельзя определять функцию с тем же именем и с той же сигнатурой параметров, но с другим типом возвращаемого значения, чем у виртуальной функции базового класса. Это приводит к ошибке на этапе компиляции.
Если в производном классе ввести функцию с тем же именем и типом возвращаемого значения, что и виртуальная функция базового класса, но с другой сигнатурой параметров, то эта функция производного класса не будет виртуальной. В этом случае с помощью указателя на базовый класс при любом значении этого указателя выполняется обращение к функции базового класса (несмотря на спецификатор virtual и присутствие в производном классе похожей функции).
Сказанное иллюстрирует следующая программа.
//OOР31_1.СРР - некоторые особенности виртуальных функций. #include <iostream.h> #include <conio.h> struct base { virtual void f1(void) { cout << "\nbase::f1"; } virtual void f2(void) { cout << "\nbase::f2"; } virtual void f3(void) { cout << "\nbase::f3"; } }; struct dir: public base { // Виртуальная функция: void f1(void) { cout << "\ndir::f1"; } // Ошибка в типе функции: // int f2(void) { cout << "\ndir::f2"; } // Невиртуальная функция: void f3(int i) { cout << "\ndir::f3::i = " << i; } }; void main(void) { base B, *pb = &B; dir D, *pd = &D; pb->f1(); pb->f2(); pb->f3(); pd->f1(); pd->f2(); // Ошибка при попытке без параметра вызвать dir::f3(int). // pd->f3(); pd->f3(0) ; pb = &D; pb->f2(); pb->f3(); // Ошибочное употребление или параметра, или указателя: // pb->f3(3); }
Обратите внимание, что три виртуальные функции базового класса по-разному воспринимаются в производном классе. dir::f1() - виртуальная функция, подменяющая функцию base::f1(). Функция base::f2() наследуется в классе dir так же, как и функция base::f3(). Функция dir::f3(int) - совершенно новая компонентная функция производного класса, никак не связанная с базовым классом. Именно поэтому невозможен вызов f3(int) через указатель на базовый класс. Виртуальные функции base::f2() и base::f3() оказались не переопределенными в производном классе dir. Поэтому при всех вызовах без параметров f3() используется только компонентная функция базового класса. Функция dir::f3(int) иллюстрирует соглашение языка о том, что если у функции производного класса набор параметров отличается от набора параметров соответствующей виртуальной функции базового класса, то это не виртуальная функция, а новый метод производного класса.
Завершая рассмотрение примера, еще раз подчеркнем, что при подмене виртуальной функции требуется полное совпадение сигнатур, имен и типов возвращаемых значений функций в базовом и производном классах.
Как уже было упомянуто, виртуальной функцией может быть только нестатическая компонентная функция. Виртуальной не может быть глобальная функция. Функция, подменяющая виртуальную, в производном классе может быть описана как со спецификатором virtual, так и без него. В обоих случаях она будет виртуальной, т.е. ее вызов возможен только для конкретного объекта. Виртуальная функция может быть объявлена дружественной (friend) в другом классе.
Механизм виртуального вызова может быть подавлен с помощью явного использования полного квалифицированного имени. Таким образом, при необходимости вызова из производного класса виртуального метода (компонентной функции) базового класса употребляется полное имя. Например,
struct base { virtual int f(int j) { return j * j; } }; struct dir: public base { int f(int i) { return base::f(i * 2); } };
На следующем шаге мы рассмотрим абстрактные классы.