Виртуальные функции и 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 является необязательным, но рекомендуемым. Оно сообщает компилятору о вашем намерении переопределить виртуальную функцию, что позволяет отлавливать ошибки, если сигнатуры не совпадают.
Как это работает? Когда класс имеет виртуальные функции, компилятор создает виртуальную таблицу (vtable). Это скрытая таблица поиска, содержащая указатели на виртуальные функции класса. Каждый объект с виртуальными функциями хранит скрытый указатель (vptr) на vtable своего класса.
Когда вы вызываете виртуальную функцию через указатель, программа ищет правильный адрес функции в vtable во время выполнения. Это называется динамической диспетчеризацией. Это добавляет небольшие накладные расходы по сравнению с обычными вызовами функций, но обеспечивает мощное полиморфное поведение.
Одно важное правило: если класс имеет виртуальные функции и будет использоваться в качестве базового класса, его деструктор также должен быть виртуальным. Это обеспечивает правильную очистку при удалении производных объектов через указатели на базовый класс:
class Animal {
public:
virtual ~Animal() {} // Виртуальный деструктор
virtual void speak() {}
};Задание
ЛегкоДавайте создадим систему медиаплеера, которая демонстрирует возможности виртуальных функций и полиморфизма времени выполнения. Вы создадите иерархию типов медиа, каждый из которых воспроизводится по-разному, и увидите, как виртуальные функции обеспечивают правильное поведение даже при доступе через указатели базового класса.
Вы организуете свой код в трех файлах:
Media.h: Определите базовый классMedia, который представляет любой воспроизводимый медиафайл:- Защищенный член
std::string title - Конструктор, который принимает заголовок и сохраняет его
- Виртуальный метод
play(), который выводит:Playing media: <title> - Виртуальный деструктор, который выводит:
Media [<title>] destroyed
- Защищенный член
AudioTrack.h: Определите классAudioTrack, который публично наследуется отMedia:- Приватный член
std::string artist - Конструктор, который принимает заголовок и исполнителя, передает заголовок базовому классу и сохраняет исполнителя
- Переопределите
play(), используя ключевое словоoverride, чтобы вывести:Playing audio: <title> by <artist> - Деструктор, который выводит:
AudioTrack [<title>] destroyed
- Приватный член
main.cpp: Считайте три входных значения (каждое на отдельной строке):- Название аудиодорожки (строка)
- Имя исполнителя (строка)
- Название видео (строка)
Определите класс
VideoClipнепосредственно в main.cpp, который публично наследуется отMedia:- Приватный член
int duration(в секундах) - Конструктор, который принимает заголовок и длительность (длительность по умолчанию 120)
- Переопределите
play(), чтобы вывести:Playing video: <title> (<duration>s) - Деструктор, который выводит:
VideoClip [<title>] destroyed
Создайте массив из трех указателей
Media*внутри области видимости блока. Динамически выделите память для:- Базового объекта
Mediaс заголовком "Generic Media" - Объекта
AudioTrackс введенным заголовком и исполнителем - Объекта
VideoClipс введенным названием видео
Пройдите циклом по массиву и вызовите
play()для каждого указателя. Затем удалите все объекты в обратном порядке. После блока выведите:Playback complete!
Например, при входных данных 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* вызывает правильный метод производного класса благодаря виртуальным функциям. Также обратите внимание, как виртуальный деструктор обеспечивает правильную очистку — при удалении через указатель базового класса запускаются деструкторы как производного, так и базового классов.
Шпаргалка
Ключевое слово virtual обеспечивает полиморфизм времени выполнения, определяя, какой метод вызывать, на основе фактического типа объекта, а не типа указателя:
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(); // Output: Woof!Ключевое слово override является необязательным, но рекомендуемым. Оно сообщает компилятору о вашем намерении переопределить виртуальную функцию, что позволяет отлавливать ошибки, если сигнатуры не совпадают.
Как работают виртуальные функции: Компилятор создает таблицу виртуальных методов (vtable), содержащую указатели на виртуальные функции класса. Каждый объект с виртуальными функциями хранит скрытый указатель (vptr) на vtable своего класса. При вызове виртуальной функции через указатель программа ищет правильный адрес функции в vtable во время выполнения. Это называется динамической диспетчеризацией.
Виртуальные деструкторы: Если класс имеет виртуальные функции и будет использоваться в качестве базового класса, его деструктор также должен быть виртуальным. Это гарантирует правильную очистку ресурсов при удалении производных объектов через указатели на базовый класс:
class Animal {
public:
virtual ~Animal() {} // Виртуальный деструктор
virtual void speak() {}
};Попробуйте сами
#include <iostream>
#include <string>
#include "Media.h"
#include "AudioTrack.h"
using namespace std;
// TODO: Определите здесь класс VideoClip, который публично наследуется от Media
// - Приватный член int duration (в секундах)
// - Конструктор принимает title и duration (значение duration по умолчанию 120)
// - Переопределите play(), чтобы вывести: Playing video: <title> (<duration>s)
// - Деструктор выводит: 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. Динамически выделите память для:
// - Базового объекта Media с заголовком "Generic Media"
// - Объекта AudioTrack с введенным заголовком и исполнителем
// - Объекта VideoClip с введенным заголовком видео
// 3. Пройдите циклом по массиву и вызовите play() для каждого указателя
// 4. Удалите все объекты в обратном порядке
// TODO: После блока выведите: 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Итоги — Обобщенный контейнер