Menu
Coddy logo textTech

Виртуальные функции и 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() {}
};
challenge icon

Задание

Легко

Давайте создадим систему медиаплеера, демонстрирующую возможности виртуальных функций и полиморфизма времени выполнения. Вы создадите иерархию типов media, в которой каждый тип воспроизводится по-своему, и увидите, как виртуальные функции обеспечивают правильное поведение даже при обращении через указатели на базовый class.

Вы организуете код в трёх файлах:

  • Media.h: Define базовый Media class, представляющий любой воспроизводимый media:
    • Защищённый member std::string title
    • constructor, который принимает заголовок и сохраняет его
    • virtual метод play(), который выводит: Playing media: <title>
    • virtual destructor, который выводит: Media [<title>] destroyed
  • AudioTrack.h: Define class AudioTrack, который публично наследуется от Media:
    • Приватный member std::string artist
    • constructor, который принимает заголовок и artist, передаёт заголовок базовому class и сохраняет artist
    • Переопределите play() с использованием override keyword, чтобы вывести: Playing audio: <title> by <artist>
    • destructor, который выводит: AudioTrack [<title>] destroyed
  • main.cpp: Считайте три input (каждый в отдельной строке):
    1. Заголовок аудиодорожки (string)
    2. Имя artist (string)
    3. Заголовок видео (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;
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование

Потренируйтесь самостоятельно: Онлайн-компилятор C++