Почему одного наследования недостаточно
На предыдущей странице вы построили иерархию классов: производный класс наследует члены от базового. Но есть нюанс. Когда вы вызываете метод через указатель на базовый класс, C++ решает, какую функцию выполнить, исходя из типа указателя, а не из реального типа объекта. Поэтому Animal*, который на самом деле указывает на Dog, всё равно вызовет версию метода Animal.
Это почти никогда не то, что вам нужно. Чаще всего у вас есть коллекция указателей на базовый класс, каждый из которых указывает на свой производный объект, и вы хотите, чтобы каждый вёл себя как он сам. Виртуальные функции делают это возможным.
Объект действительно является Dog, и всё же a->speak() выполнил Animal::speak(). Поскольку speak не виртуальная, компилятор выбрал функцию на этапе компиляции по статическому типу Animal*. Именно эту ошибку и призваны исправить виртуальные функции.
Как сделать функцию виртуальной
Добавьте ключевое слово virtual к методу базового класса. Теперь вызов разрешается во время выполнения на основе реального типа объекта - это динамическая диспетчеризация.
Один цикл по Animal* - три разных поведения. Указатель на базовый класс «знает» реальный тип во время выполнения и диспетчеризует соответственно. Этот единственный механизм - один интерфейс, множество реализаций - и есть то, что в C++ называется полиморфизмом.
Обратите внимание: virtual нужно указывать только в объявлении базового класса; как только функция стала виртуальной, она автоматически остаётся виртуальной в каждом производном классе. Писать его снова в производном классе необязательно и избыточно.
Всегда используйте ключевое слово override
В примере выше каждый производный метод помечен override. Для работы кода это необязательно, но относиться к этому стоит как к обязательному. override (C++11) просит компилятор проверить, что вы действительно переопределяете виртуальную функцию базового класса с совпадающей сигнатурой. Если вы тонко ошибётесь в сигнатуре, вы получите понятную ошибку вместо тихого бага.
struct Animal {
virtual void speak() const { } // обратите внимание: const
};
struct Dog : Animal {
void speak() { } // НЕ const - это НОВАЯ функция, а не переопределение!
void speak() override { } // ошибка: 'speak' ничего не переопределяет - сообщает сразу
};
Без override первый speak() компилируется без проблем, но никогда не вызывается через Animal*, потому что его сигнатура отличается от базовой (нет const). Вы потратите целый вечер, гадая, почему ваше переопределение ничего не делает. С override компилятор ловит несоответствие на месте. Добавляйте его к каждой переопределяющей функции.
Чисто виртуальные функции и абстрактные классы
Иногда у базового класса нет разумного значения по умолчанию - какой звук издаёт обобщённое «Animal»? В этом случае объявите функцию чисто виртуальной, присвоив = 0. Это оставляет её без тела и превращает класс в абстрактный класс, экземпляр которого нельзя создать сам по себе. Он существует только для того, чтобы определить интерфейс, который должны реализовать производные классы.
Каждый конкретный подкласс обязан реализовать area(), иначе он тоже остаётся абстрактным. Именно так C++ выражает «интерфейсы»: абстрактный класс, содержащий только чисто виртуальные функции, - это эквивалент в C++ интерфейса в языках вроде Java.
Правило виртуального деструктора
Это та ловушка, в которую хоть раз попадает каждый. Когда вы выполняете delete объекта через указатель на базовый класс, C++ вызывает найденный деструктор - и если этот деструктор не виртуальный, выполняется только деструктор базового класса. Производная часть никогда не разрушается, утекает всё, чем она владела. Стандарт называет это неопределённым поведением.
Исправление - одно слово: сделайте деструктор базового класса virtual. Тогда delete p выполнит сначала ~Derived, затем ~Base, ровно как и должно быть.
struct Base {
virtual ~Base() { cout << "~Base\n"; } // правильно
};
// теперь: ~Derived, затем ~Base
Правило большого пальца: как только у класса появляется хоть одна виртуальная функция, дайте ему и виртуальный деструктор. Если класс предназначен быть базовым и использоваться через указатели, его деструктор должен быть виртуальным.
Распространённые ошибки и подводные камни
Ещё несколько ловушек, на которые стоит обратить внимание, когда вы освоитесь с виртуальными функциями:
Срезка объекта (object slicing). Если вы передаёте или храните производный объект по значению в переменной базового типа, производная часть «срезается», и у вас остаётся обычный базовый объект - виртуальная диспетчеризация больше не достигает переопределения. Для полиморфизма всегда используйте указатели или ссылки:
Dog d;
Animal a = d; // СРЕЗАНО: a теперь просто Animal, часть Dog исчезла
a.speak(); // выполняет Animal::speak, хотя функция виртуальная
Animal& ref = d; // OK: ссылка сохраняет реальный тип
ref.speak(); // выполняет Dog::speak
Не вызывайте виртуальные функции из конструкторов или деструкторов. Во время конструирования производная часть ещё не существует, поэтому виртуальный вызов разрешается в версию текущего класса, а не в производное переопределение - редко то, что вы задумывали.
Виртуальная диспетчеризация имеет небольшую цену. Каждый виртуальный вызов проходит через скрытую таблицу указателей на функции («vtable»), одна косвенность на вызов. Это дёшево, но не бесплатно, поэтому не делайте функцию виртуальной, если переопределение вам на самом деле не нужно.
Намеренный вызов базовой версии. Внутри переопределения вы всё ещё можете явно вызвать реализацию базового класса через Base::method() - полезно, когда производное поведение расширяет, а не заменяет базовое.
Далее: Перегрузка операторов
Виртуальные функции позволяют вашим объектам настраивать своё поведение через общий интерфейс. На следующей странице показано, как настроить операторы, которые действуют на ваши объекты: с помощью перегрузки операторов вы можете научить собственные типы реагировать на +, ==, << и другие, так что Vector + Vector или cout << myObject будут читаться так же естественно, как и для встроенных типов.
Часто задаваемые вопросы
Что такое виртуальная функция в C++?
Виртуальная функция - это функция-член, объявленная с ключевым словом virtual в базовом классе, так что при вызове через указатель или ссылку на базовый класс C++ выполняет переопределённую версию производного класса вместо версии базового. Этот выбор во время выполнения называется динамической диспетчеризацией (dynamic dispatch) и является основой полиморфизма.
В чём разница между виртуальной и чисто виртуальной функцией?
Виртуальная функция имеет тело и может быть переопределена. Чисто виртуальная функция объявляется с = 0 и не имеет тела в базовом классе - она заставляет каждый конкретный производный класс предоставить реализацию. Любой класс, имеющий хотя бы одну чисто виртуальную функцию, является абстрактным классом, и его экземпляр создать нельзя.
Почему базовому классу в C++ нужен виртуальный деструктор?
Если вы выполняете delete производного объекта через указатель на базовый класс, а деструктор базового класса не виртуальный, выполняется только деструктор базового класса - производная часть никогда не очищается, что приводит к утечкам ресурсов и является неопределённым поведением. Сделайте деструктор любого класса, предназначенного для полиморфного использования, virtual.