Что такое виртуальная функция в C++?

Ответ

Виртуальная функция — это функция-член класса, объявленная с ключевым словом virtual. Её вызов разрешается не на этапе компиляции (статическое связывание), а во время выполнения программы (динамическое связывание) на основе фактического типа объекта, на который указывает указатель или ссылка. Это основа для реализации полиморфизма в C++.

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

Ключевые аспекты:

  • Переопределение: В производном классе функция с той же сигнатурой переопределяет базовую. Начиная с C++11, рекомендуется использовать спецификатор override для явного указания намерения.
  • Чисто виртуальные функции: Функция, объявленная как virtual ... = 0;. Класс, содержащий такую функцию, становится абстрактным классом, и его нельзя инстанцировать. Производные классы обязаны предоставить её реализацию.
  • Виртуальный деструктор: Если класс предназначен для наследования и полиморфного использования через указатель на базовый класс, его деструктор должен быть объявлен виртуальным. Иначе при удалении объекта через указатель на базовый класс вызовется деструктор базового класса, но не деструктор производного, что приведет к утечке ресурсов.

Пример:

#include <iostream>

class Shape {
public:
    // Виртуальная функция для полиморфного поведения
    virtual void draw() const {
        std::cout << "Drawing a generic shape.n";
    }
    // Виртуальный деструктор - КРИТИЧНО для корректного удаления
    virtual ~Shape() = default;
};

class Circle : public Shape {
public:
    // Переопределение виртуальной функции
    void draw() const override { // `override` гарантирует, что функция виртуальна в базовом классе
        std::cout << "Drawing a circle.n";
    }
};

class Rectangle : public Shape {
public:
    void draw() const override {
        std::cout << "Drawing a rectangle.n";
    }
};

int main() {
    Shape* shapes[] = {new Circle(), new Rectangle()};

    for (Shape* s : shapes) {
        s->draw(); // Вызовется draw() для фактического типа объекта
        // Вывод:
        // Drawing a circle.
        // Drawing a rectangle.
    }

    for (Shape* s : shapes) {
        delete s; // Благодаря виртуальному деструктору вызывается правильный деструктор
    }
    return 0;
}

Ответ 18+ 🔞

А, виртуальные функции! Ну это, блядь, классика такая, ёпта. Сидишь ты, пишешь код, и тут бац — нужно, чтобы один и тот же вызов делал разную херню в зависимости от объекта. Вот тут-то они и выезжают, эти самые virtual.

Представь, у тебя есть класс Shape, ну, фигура там. И ты объявляешь в нём функцию draw() с этим самым словечком virtual. Это как сказать компилятору: «Слушай, чувак, я тут нарисую какую-то общую хуйню, но если кто-то от меня унаследуется — пусть делает что хочет, я не в обиде». И компилятор такой: «Окей, я тебя понял».

А как оно внутри-то работает, ёпта? А вот тут начинается магия, я тебе скажу. Компилятор, когда видит virtual, для каждого такого класса создаёт скрытую таблицу — vtable. Это типа менюшка, где написано: «Функция draw() — вот по этому адресу лежит её код». А каждый объект этого класса получает себе скрытый указатель (vptr), который тыкает именно в менюшку своего класса. И когда ты через указатель на базовый класс вызываешь draw(), программа во время выполнения смотрит: «Ага, у этого объекта vptr ведёт в меню класса Circle — значит, бери адрес оттуда и прыгай!» Вот это и есть динамическое связывание, ёбана. Не на этапе компиляции всё решается, а прямо в рантайме. Удивление пиздец, правда?

На что обратить внимание, чтобы не облажаться:

  • Переопределение (override): В дочернем классе пишешь функцию с той же самой сигнатурой — и она автоматом перекрывает родительскую. Но чтобы не было вот этого «ой, а я опечатался в названии и нихуя не переопределил», с C++11 есть волшебное слово override. Пишешь его — и компилятор тебя страхует: если в базовом классе нет такой виртуальной функции, он тебе в морду ошибкой кинет. Доверия ебать ноль к себе самому, поэтому всегда пиши override.
  • Чисто виртуальные функции: Это когда ты в базовом классе забиваешь болт на реализацию и пишешь = 0. Типа: «Ребята, я тут абстрактный, меня инстанциировать нельзя. Кто наследует — тот сам и разбирайтесь, как это рисовать». Класс сразу становится абстрактным. Если попробуешь создать объект — получишь хитрую жопу от компилятора.
  • Виртуальный деструктор — это святое! Вот это, блядь, самая важная хуйня. Если ты делаешь полиморфную иерархию и планируешь удалять объекты через указатель на базовый класс, деструктор в базе обязан быть виртуальным. Иначе будет пиздец. Удалишь ты Circle* через Shape*, вызовется деструктор только для Shape, а память, которую Circle себе выделял, останется висеть. Утечка, анал. Терпения ноль ебать к тем, кто это забывает. Просто объяви деструктор виртуальным и спи спокойно. Или сделай его чисто виртуальным (тогда всё равно нужна реализация!), если класс чисто абстрактный.

Смотри, как это в коде выглядит:

#include <iostream>

class Shape {
public:
    // Виртуальная функция для полиморфного поведения
    virtual void draw() const {
        std::cout << "Drawing a generic shape.n";
    }
    // Виртуальный деструктор - КРИТИЧНО для корректного удаления
    virtual ~Shape() = default;
};

class Circle : public Shape {
public:
    // Переопределение виртуальной функции
    void draw() const override { // `override` гарантирует, что функция виртуальна в базовом классе
        std::cout << "Drawing a circle.n";
    }
};

class Rectangle : public Shape {
public:
    void draw() const override {
        std::cout << "Drawing a rectangle.n";
    }
};

int main() {
    Shape* shapes[] = {new Circle(), new Rectangle()};

    for (Shape* s : shapes) {
        s->draw(); // Вызовется draw() для фактического типа объекта
        // Вывод:
        // Drawing a circle.
        // Drawing a rectangle.
    }

    for (Shape* s : shapes) {
        delete s; // Благодаря виртуальному деструктору вызывается правильный деструктор
    }
    return 0;
}

Видишь? В цикле мы тыкаем в указатели на Shape, но вызывается draw() у конкретных Circle и Rectangle. Полиморфизм, сука, в действии. А в конце удаляем — и благодаря виртуальному деструктору всё чистится как надо, без сюрпризов. Ёперный театр, а ведь удобно! Главное — не проеби этот деструктор.