После того, как функция объявлена виртуальной, она остается таковой независимо от того, через сколько производных классов она прошла. Например, если класс D2 объявлен производным не от В, а от D1, как это показано в следующем фрагменте, тогда функция все равно остается виртуальной:
// D2 производный от D1, а не от В..
class D2 : public Dl {
public:
void who() { // Определение who() в классе D2
cout << "Второй производный класс\n";
}
};
Если производный класс не переопределяет виртуальную функцию, тогда используется функция в том виде, в каком она определена в базовом классе. В качестве примера испытайте приведенный ниже вариант предыдущей программы. В нем класс D2 не переопределяет функцию who():
#include <iostream>
#include <conio>
using namespace std;
class B {
public:
virtual void who() {
cout << "Base класс\n";
}
};
class D1 : public B {
void who() {
cout << "First derived class \n";
}
};
class D2 : public B {// D2 не переопределяет who().
// who() не определена
};
int main () {
B base_obj;
B *p;
D1 D1_obj;
D2 D2_obj;
p = &base_obj;
p->who(); // обращение к who() из В
p = &D1_obj;
p->who(); // обращение к who () из D1
p = &D2_obj;
p->who() ; /* обращение к who () из В, потому что D2 не переопределил who()*/
getch();
return 0;
}
Вот вывод этой программы:
Базовый класс
Первый базовый класс
Базовый класс
Из-за того, что класс D2 не переопределил who(), используется вариант who() , определенный в базовом классе.
Не забывайте, что наследование виртуальных функций имеет иерархический характер. Если пример изменить так, чтобы D2 был производным не от В, а от D1, тогда при вызове функции who() для объекта типа D2 будет вызываться не who() из класса В, а вариант who(), объявленный внутри D1, потому что класс D1 является ближайшим классом к D2 в иерархии классов.
Практический пример использования виртуальных функций
Для того, чтобы продемонстрировать мощь виртуальных функций, мы используем их в классе TwoDShape. Определим виртуальную функцию с именем агеа() в базовом классе TwoDShape. При этом позволяем каждому производному классу переопределить эту функцию, поместив в нее формулу для вычисления площади того типа фигур, которые инкапсулирует данный класс. Приводимая ниже программа иллюстрирует этот подход. Кроме того, в ней для удобства в класс TwoDShape включено поле с названием фигуры. (Это облегчает демонстрацию классов.)
// Использование виртуальных функций и полиморфизма.
#include <iostream>
#include <cstring>
#include <conio>
using namespace std;
// Класс для двумерных объектов.
class TwoDShape {
// эти члены закрыты
double width;
double height;
// добавим поле для названия фигуры
char name[20];
public :
// Конструктор по умолчанию.
TwoDShape() {
width = height = 0.0;
strcpy(name, "неизвестно");
}
// Конструктор для TwoDShape.
TwoDShape(double w, double h, char *n) {
width = w;
height = h;
strcpy(name, n);
}
// Конструируем объект с равными шириной и высотой.
TwoDShape (double x, char *n) {
width = height = x;
strcpy(name, n);
}
void showDim() {
cout << "Ширина и высота составляют "
<< width << " и " << height << "\n";
}
// функции доступа
double getWidth() { return width; }
double getHeight() { return height; }
void setWidth (double w) { width = w; }
void setHeight (double h) { height = h; }
char *getName() { return name; }
// Добавим в TwoDShape функцию area()и сделаем
// ее виртуальной.
virtual double area () { //Функция агеа( ) теперь виртуальная.
cout << "Ошибка: area() должна быть переопределена.\n";
return 0.0;
}
};
// Triangle является производным от TwoDShape.
class Triangle : public TwoDShape {
char style[20]; // теперь private
public:
/* Конструктор по умолчанию. Он автоматически вызывает
for(int i=0; i < 5; i++) { // Теперь для каждого объекта вызываем
//правильный вариант функции агеа()
cout << "объект представляет собой " <<
shapes[i]->getName() << "\n";
cout << "Площадь равна " <<
shapes[i]->area() << "\n";
cout << "\n";
}
getch();
return 0;
}
Ниже приведен вывод этой программы:
объект представляет собой треугольник
Площадь равна 48
объект представляет собой прямоугольник
Площадь равна 100
объект представляет собой прямоугольник
Площадь равна 40
объект представляет собой треугольник
Площадь равна 24.5
объект представляет собой обобщенную фигуру
Ошибка: area() должна быть переопределена.
Площадь равна 0
Рассмотрим программу в деталях. Прежде всего, агеа( ) объявлена как виртуальная в классе TwoDShape и переопределена в классах Triangle и Rectangle. Внутри TwoDShape для функции агеа() предусмотрена реализация-заготовка, которая просто информирует пользователя, что функция должна быть переопределена в производном классе. Каждое переопределение агеа( ) представляет реализацию, отвечающую типу объекта, инкапсулированного в данном производном классе. Таким образом, если бы вы, например, реализовывали класс эллипсов, тогда агеа() должна была бы вычислять площадь эллипса.
В рассмотренной программе имеется одно важное место. Обратите внимание на то, что переменная shapes в main() объявлена как массив указателей на объекты TwoDShape. Однако элементам этого массива присвоены указатели на объекты Triangle, Rectangle и TwoDShape. Это допустимо, потому что указатель базового класса может указывать на объект производного класса. Программа затем просматривает этот массив в цикле, выводя информацию о каждом объекте. Будучи весьма простым, этот фрагмент тем не менее иллюстрирует мощь как наследования, так и виртуальных функций. Тип объекта, на который указывает указатель базового класса, определяется во время выполнения, и дальнейшие действия зависят от этого типа. Если объект является производным от TwoShape, его площадь можно получить, вызвав функцию агеа(), причем интерфейс этой операции одинаков, для какого бы типа фигуры он не использовался.