Для того чтобы обработать вызов виртуальной функции, нам нужна еще одна порция данных в объекте класса
Shape
: информация о том, какая функция будет на самом деле вызываться при обращении к функции
draw_lines()
из класса
Shape
. Для этого обычно в таблицу функций заносится ее адрес. Эта таблица обычно называется
vtbl
(таблица виртуальных функций), а ее адрес часто имеет имя
vptr
(виртуальный указатель). Указатели обсуждаются в главах 17-18; здесь они действуют как ссылки. В конкретных реализациях языка таблица виртуальных функций и виртуальный показатель могут называться иначе. Добавив таблицу
vptr
и указатели
vtbl
к нашему рисунку, получим следующую диаграмму.
Поскольку функция
draw_lines()
— первая виртуальная функция, она занимает первую ячейку в таблице
vtbl
, за ней следует функция
move()
, вторая виртуальная функция. Класс может иметь сколько угодно виртуальных функций; его таблица
vtbl
может быть сколь угодно большой (по одной ячейке на каждую виртуальную функцию). Теперь, когда мы вызовем функцию
x.draw_lines()
, компилятор сгенерирует вызов функции, найденной в ячейке
draw_lines()
таблицы
vtbl
, соответствующей объекту
x
. В принципе код просто следует по стрелкам на диаграмме.
Итак, если объект
x
относится к классу
Circle
, будет вызвана функция
Circle::draw_lines()
. Если объект
x
относится к типу, скажем,
Open_polyline
, который использует таблицу
vtbl
точно в том виде, в каком ее определил класс
Shape
, то будет вызвана функция
Shape::draw_lines()
. Аналогично, поскольку в классе
Circle
не определена его собственная функция
move()
, при вызове
x.move()
будет выполнена функция
Shape::move()
, если объект
x
относится к классу
Circle
. В принципе код, сгенерированный для вызова виртуальной функции, может просто найти указатель
vptr
и использовать его для поиска соответствующей таблицы
vtbl
и вызова нужной функции оттуда. Для этого понадобятся два обращения к памяти и обычный вызов функции, — быстро и просто.
Класс
Shape
является абстрактным, поэтому мы не можем на самом деле непосредственно создать объект класса
Shape
, но класс
Open_polyline
имеет точно такую же простую структуру, поскольку не добавляет никаких данных-членов и не определяет виртуальную функцию. Таблица виртуальных функций
vtbl
определяется для каждого класса, в котором определена виртуальная функция, а не для каждого объекта, поэтому таблицы
vtbl
незначительно увеличивают размер программы.
Обратите внимание на то, что на рисунке мы не изобразили ни одной невиртуальной функции. В этом не было необходимости, поскольку об этих функциях мы не можем сказать что-то особенное и они не увеличивают размеры объектов своего класса. Определение функции, имеющей то же имя и те же типы аргументов, что и виртуальная функция из базового класса (например,
Circle::draw_lines()
), при котором функция из производного класса записывается в таблицу
vtbl
вместо соответствующей функции из базового класса, называется
замещением (overriding). Например, функция
Circle::draw_lines()
замещает функцию
Shape::draw_lines()
.
Почему мы говорим о таблицах
vtbl
и схемах размещения в памяти? Нужна ли нам эта информация, чтобы использовать объектно-ориентированное программирование? Нет. Однако многие люди очень хотят знать, как устроены те или иные механизмы (мы относимся к их числу), а когда люди чего-то не знают, возникают мифы. Мы встречали людей, которые боялись использовать виртуальные функции, “потому что они повышают затраты”. Почему? Насколько? По сравнению с чем? Как оценить эти затраты? Мы объяснили модель реализации виртуальных функций, чтобы вы их не боялись. Если вам нужно вызвать виртуальную функцию (для выбора одной из нескольких альтернатив в ходе выполнения программы), то вы не сможете запрограммировать эту функциональную возможность с помощью другого языкового механизма, который работал бы быстрее или использовал меньше памяти, чем механизм виртуальных функций. Можете сами в этом убедиться.
14.3.2. Вывод классов и определение виртуальных функций
Мы указываем, что класс является производным, упоминая базовый класс перед его именем. Рассмотрим пример.
struct Circle:Shape { /* ... */ };
По умолчанию члены структуры, объявляемой с помощью ключевого слова
struct
, являются открытыми (см. раздел 9.3) и наследуют открытые члены класса. Можно было бы написать эквивалентный код следующим образом:
class Circle : public Shape { public: /* ... */ };
Эти два объявления класса
Circle
совершенно эквивалентны, но вы можете провести множество долгих и бессмысленных споров о том, какой из них лучше. Мы считаем, что время, которое можно затратить на эти споры, лучше посвятить другим темам.
Не забудьте указать слово
public
, когда захотите объявить открытые члены класса. Рассмотрим пример.
class Circle : Shape { public: /* ... */ }; // возможно, ошибка
В этом случае класс
Shape
считается закрытым базовым классом для класса
Circle
, а открытые функции-члены класса
Shape
становятся недоступными для класса
Circle
. Вряд ли вы стремились к этому. Хороший компилятор предупредит вас о возможной ошибке. Закрытые базовые классы используются, но их описание выходит за рамки нашей книги.
Виртуальная функция должны объявляться с помощью ключевого слова
virtual
в объявлении своего класса, но если вы разместили определение функции за пределами класса, то ключевое слово
virtual
указывать не надо.