Виртуальные функции и VTable
Часть раздела Объектно-ориентированное программирование путешествия по C++ на Coddy. Урок 52 из 104.
Ключевое слово virtual решает проблему, которую мы рассмотрели на предыдущем уроке. Когда вы объявляете метод виртуальным, C++ определяет, какую версию вызвать, на основе фактического типа объекта во время выполнения, а не типа указателя.
class Animal {
public:
virtual void speak() {
std::cout << "Some sound" << std::endl;
}
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
};
Dog d;
Animal* ptr = &d;
ptr->speak(); // Вывод: Woof!Теперь вызывается правильный метод! Ключевое слово override необязательно, но рекомендуется. Оно сообщает компилятору, что вы намерены переопределить виртуальную функцию, и позволяет обнаружить ошибки, если сигнатуры не совпадают.
Как это работает? Когда class содержит виртуальные функции, компилятор создаёт таблицу виртуальных функций (vtable). Это скрытая таблица поиска, содержащая указатели на виртуальные функции class. Каждый объект с виртуальными функциями хранит скрытый указатель (vptr) на таблицу виртуальных функций своего class.
Когда вы вызываете виртуальную функцию через указатель, программа во время выполнения ищет правильный адрес функции в vtable. Это называется динамической диспетчеризацией. По сравнению с обычными вызовами функций это добавляет небольшие накладные расходы, но обеспечивает мощное полиморфное поведение.
Одно важное правило: если class содержит виртуальные функции и будет использоваться как base, его destructor также должен быть виртуальным. Это обеспечивает правильную очистку при удалении производных объектов через указатели на base:
class Animal {
public:
virtual ~Animal() {} // Виртуальный деструктор
virtual void speak() {}
};Задание
ЛегкоДавайте создадим систему медиаплеера, демонстрирующую возможности виртуальных функций и полиморфизма времени выполнения. Вы создадите иерархию типов media, в которой каждый тип воспроизводится по-своему, и увидите, как виртуальные функции обеспечивают правильное поведение даже при обращении через указатели на базовый class.
Вы организуете код в трёх файлах:
Media.h: Define базовыйMediaclass, представляющий любой воспроизводимый media:- Защищённый member
std::string title - constructor, который принимает заголовок и сохраняет его
- virtual метод
play(), который выводит:Playing media: <title> - virtual destructor, который выводит:
Media [<title>] destroyed
- Защищённый member
AudioTrack.h: Define classAudioTrack, который публично наследуется отMedia:- Приватный member
std::string artist - constructor, который принимает заголовок и artist, передаёт заголовок базовому class и сохраняет artist
- Переопределите
play()с использованиемoverridekeyword, чтобы вывести:Playing audio: <title> by <artist> - destructor, который выводит:
AudioTrack [<title>] destroyed
- Приватный member
main.cpp: Считайте три input (каждый в отдельной строке):- Заголовок аудиодорожки (string)
- Имя artist (string)
- Заголовок видео (string)
Define class
VideoClipнепосредственно в main.cpp, публично наследующийся отMedia:- Приватный member
int duration(в секундах) - constructor, который принимает заголовок и duration (default duration равен 120)
- Переопределите
play(), чтобы вывести:Playing video: <title> (<duration>s) - destructor, который выводит:
VideoClip [<title>] destroyed
Create array из трёх указателей
Media*внутри области видимости block. Dynamically выделите:- Объект базового
Mediaс заголовком "Generic Media" - Объект
AudioTrackс input-заголовком и artist - Объект
VideoClipс input-заголовком видео
Выполните Loop по array и вызовите
play()для каждого указателя. Затем удалите все объекты в обратном порядке. После block выведите:Playback complete!
Например, при input Bohemian Rhapsody, Queen и Nature Documentary:
Playing media: Generic Media
Playing audio: Bohemian Rhapsody by Queen
Playing video: Nature Documentary (120s)
VideoClip [Nature Documentary] destroyed
Media [Nature Documentary] destroyed
AudioTrack [Bohemian Rhapsody] destroyed
Media [Bohemian Rhapsody] destroyed
Media [Generic Media] destroyed
Playback complete!Обратите внимание: вызов play() через указатели Media* вызывает метод правильного производного class благодаря виртуальным функциям. Также обратите внимание, как virtual destructor обеспечивает корректную очистку: при удалении через указатель на базовый class вызываются как производный, так и базовый destructors.
Попробуйте сами
#include <iostream>
#include <string>
#include "Media.h"
#include "AudioTrack.h"
using namespace std;
// TODO: Определите класс VideoClip здесь, который публично наследуется от Media
// - Приватный член int duration (в секундах)
// - Конструктор принимает title и duration (duration по умолчанию 120)
// - Override play() to print: Playing video: <title> (<duration>s)
// - Destructor prints: VideoClip [<title>] destroyed
class VideoClip : public Media {
private:
int duration;
public:
// TODO: Реализуйте конструктор
VideoClip(const std::string& t, int d = 120) : Media(t) {
// TODO: Сохраните duration
}
// TODO: Переопределите метод play()
void play() override {
// TODO: Реализуйте этот метод
}
// TODO: Реализуйте деструктор
~VideoClip() {
// TODO: Реализуйте этот деструктор
}
};
int main() {
// Прочитайте входные данные
string audioTitle;
string artist;
string videoTitle;
getline(cin, audioTitle);
getline(cin, artist);
getline(cin, videoTitle);
// TODO: Создайте область видимости блока с фигурными скобками
// Внутри блока:
// 1. Создайте массив из трёх указателей Media*
// 2. Динамически выделите:
// - A base Media object with title "Generic Media"
// - Объект AudioTrack с введённым названием и исполнителем
// - Объект VideoClip с введённым названием видео
// 3. Пройдите по массиву и вызовите play() для каждого указателя
// 4. Удалите все объекты в обратном порядке
// TODO: After the block, print: Playback complete!
return 0;
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП
Внешние файлыСборка и компиляция C++Заголовочные файлы и файлы исходного кодаПространства имен и область видимостиВведение в ООП на C++Классы и объектыУказатель 'this'Методы (функции-члены)Атрибуты (члены данных)Основы конструкторов и деструкторовИтоги — Простой калькулятор4Свойства классов
Члены экземпляра против статическихГеттеры и сеттерыКонстантные функции-членыКлючевое слово mutableСтатические методы и переменныеДружественные функции и классыИтоги — Менеджер банковских счетов7Наследование
Основы наследованияУровни доступа при наследованииПорядок вызова конструкторов и деструкторовПереопределение методовВиртуальные функции и VTableМножественное наследованиеВиртуальное наследованиеИтоги — Иерархия сотрудников10Обзор STL
Обзор и философия STLКонтейнеры STLИтераторыАлгоритмы STLФункторы и лямбда-выраженияИтоги: частота слов13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonFactory и Abstract FactoryПаттерн BuilderПаттерн ObserverПаттерн Strategy2Управление памятью
Стек и кучаУказатели и ссылкиДинамическая память (new/delete)Умные указатели в C++RAII в C++Итоги — Менеджер динамического массива5Инкапсуляция
Спецификаторы доступа в C++Спецификаторы доступа: подробный разборСокрытие данныхStruct против ClassВложенные и внутренние классыИтоги — Система учета студентов8Полиморфизм
Полиморфизм компиляции и времени выполненияПерегрузка функцийВиртуальные функции: повторениеЧистые виртуальные функцииАбстрактные классыПроектирование интерфейсов на C++Dynamic Casting и RTTIИтоги: Калькулятор фигур11Продвинутые концепции ООП
Композиция против наследованияМиксины через CRTPИдиома PimplСтирание типовEnum Classes и строгая типизацияОбработка исключений в ООППользовательские иерархии исключений14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикRAII как паттерн3Конструкторы и деструкторы
Конструктор по умолчаниюПараметризованный конструкторКонструктор копированияКонструктор перемещенияСписки инициализации конструктораДелегирующие конструкторыГлубокое погружение в деструкторыПравило трех / пяти / нуляПовторение — класс String6Перегрузка операторов
Введение в перегрузку операторовПерегрузка арифметических операторовПерегрузка операторов сравненияОператоры потоковПерегрузка оператора присваиванияПерегрузка операторов [] и ()Операторы преобразования типовИтоги — класс Matrix9Шаблоны
Шаблоны функцийШаблоны классовСпециализация шаблоновВариативные шаблоныОсновы SFINAE и Type TraitsИтоги — Обобщенный контейнерПотренируйтесь самостоятельно: Онлайн-компилятор C++