Как в C++ реализуется полиморфизм?

Ответ

В C++ полиморфизм времени выполнения реализуется через виртуальные функции и наследование. Механизм основан на таблице виртуальных функций (vtable) и указателе на нее (vptr) в каждом объекте полиморфного класса.

Базовый пример:

#include <iostream>

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

class Circle : public Shape {
public:
    // Ключевое слово 'override' явно указывает на переопределение (проверяется компилятором)
    void draw() const override {
        std::cout << "Drawing a circle.n";
    }
};

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

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

    for (Shape* s : shapes) {
        s->draw(); // Вызовется правильная версия draw() для каждого типа
        delete s;  // Корректное удаление благодаря виртуальному деструктору
    }
    return 0;
}

Ключевые элементы:

  1. virtual в базовом классе: помечает функцию как переопределяемую.
  2. override (C++11) в производном классе: гарантирует, что функция переопределяет виртуальную функцию базового класса.
  3. Виртуальный деструктор: Обязателен, если возможна работа с объектами через указатель на базовый класс и их удаление.
  4. Чисто виртуальные функции и абстрактные классы: virtual void draw() const = 0; делает класс Shape абстрактным, его нельзя инстанцировать.

Полиморфизм работает только при вызове через указатель или ссылку на базовый класс.

Ответ 18+ 🔞

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

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

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";
    }
};

И вот тут самый сок. Создаёшь массив указателей на базовый класс Shape, но пихаешь в него на самом деле круги да квадраты.

Shape* shapes[] = {new Circle(), new Square()};

for (Shape* s : shapes) {
    s->draw(); // Вот тут-то и происходит волшебство!
    delete s;
}

Когда ты вызываешь s->draw(), компилятор не тупо лезет в код класса Shape. Нет! Он смотрит, а на что на самом деле указывает этот указатель s? Ага, на Circle! И тогда он идёт в специальную табличку — таблицу виртуальных функций (vtable), которая есть у каждого такого объекта, и вызывает правильную, переопределённую функцию для круга. Для квадрата — свою. Это и есть полиморфизм времени выполнения. Сам от себя охуеешь, когда впервые поймёшь, как это элегантно устроено.

А если хочешь сделать класс чисто абстрактным, чтобы его вообще нельзя было создавать, объявляешь функцию чисто виртуальной:

virtual void draw() const = 0; // Всё, теперь Shape — это просто идея, контракт для потомков.

И запомни главное: этот фокус работает только когда ты работаешь через указатель или ссылку на базовый класс. Если создашь объект Circle просто как Circle c; и вызовешь c.draw() — никакой магии, вызовется сразу метод круга. Вся эта система с vtable включается только когда есть неопределённость, когда компилятор в момент компиляции не может точно знать, что за объект придёт. Вот такая, блядь, хитрая жопа у полиморфизма в C++. Кажется сложным, но как разберёшься — пизда рулю, очень мощная штука.