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 является необязательным, но рекомендуемым. Оно сообщает компилятору о вашем намерении переопределить виртуальную функцию, что позволяет отлавливать ошибки, если сигнатуры не совпадают.

Как это работает? Когда класс имеет виртуальные функции, компилятор создает виртуальную таблицу (vtable). Это скрытая таблица поиска, содержащая указатели на виртуальные функции класса. Каждый объект с виртуальными функциями хранит скрытый указатель (vptr) на vtable своего класса.

Когда вы вызываете виртуальную функцию через указатель, программа ищет правильный адрес функции в vtable во время выполнения. Это называется динамической диспетчеризацией. Это добавляет небольшие накладные расходы по сравнению с обычными вызовами функций, но обеспечивает мощное полиморфное поведение.

Одно важное правило: если класс имеет виртуальные функции и будет использоваться в качестве базового класса, его деструктор также должен быть виртуальным. Это обеспечивает правильную очистку при удалении производных объектов через указатели на базовый класс:

class Animal {
public:
    virtual ~Animal() {}  // Виртуальный деструктор
    virtual void speak() {}
};
challenge icon

Задание

Легко

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

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

  • 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: Считайте три входных значения (каждое на отдельной строке):
    1. Название аудиодорожки (строка)
    2. Имя исполнителя (строка)
    3. Название видео (строка)

    Определите класс 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;
}
quiz iconПроверьте себя

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

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