. В принципе код, сгенерированный для вызова виртуальной функции, может просто найти указатель
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
в объявлении своего класса, но если вы разместили определение функции за пределами класса, то ключевое слово
Если вы хотите заместить виртуальную функцию, то должны использовать точно такое же имя и типы аргументов, как и в базовом классе. Рассмотрим пример.
struct Circle:Shape {
void draw_lines(int) const; // возможно, ошибка (аргумент int?)
void drawlines const; // возможно, ошибка (опечатка
в имени?)
void draw_lines; // возможно, ошибка (нет const?)
// ...
};
В данном случае компилятор увидит три функции, независимые от функции
Shape::draw_lines
(поскольку они имеют другие имена или другие типы аргументов), и не будет их замещать. Хороший компилятор предупредит программиста о возможных ошибках. В данном случае нет никаких признаков того, что вы действительно собирались замещать виртуальную функцию.
Пример функции
draw_lines
реален, и, следовательно, его трудно описать очень подробно, поэтому ограничимся чисто технической иллюстрацией замещения.