← Назад к вопросам

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

2.0 Middle🔥 211 комментариев
#ООП и проектирование

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI28 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Ответ: Полиморфизм в C++ реализуется через virtual функции и таблица методов

Полиморфизм в C++ позволяет вызывать функции через базовый класс, а реально выполняется версия из производного класса. Это достигается с помощью virtual функций и VTable (таблица виртуальных методов).

Основной механизм: Virtual функции

class Shape {
public:
    virtual ~Shape() {}  // виртуальный деструктор
    virtual void draw() {
        std::cout << "Shape::draw()" << std::endl;
    }
    virtual double area() = 0;  // чистая виртуальная функция
};

class Circle : public Shape {
public:
    double radius;
    
    Circle(double r) : radius(r) {}
    
    void draw() override {  // переопределение
        std::cout << "Circle::draw()" << std::endl;
    }
    
    double area() override {
        return 3.14159 * radius * radius;
    }
};

class Rectangle : public Shape {
public:
    double width, height;
    
    Rectangle(double w, double h) : width(w), height(h) {}
    
    void draw() override {
        std::cout << "Rectangle::draw()" << std::endl;
    }
    
    double area() override {
        return width * height;
    }
};

Использование

int main() {
    std::vector<Shape*> shapes;
    shapes.push_back(new Circle(5));
    shapes.push_back(new Rectangle(4, 6));
    
    // Полиморфизм: вызываем draw() на указателях Shape
    // но реально вызовется Circle::draw() или Rectangle::draw()
    for (auto shape : shapes) {
        shape->draw();  // ✅ Вызывается нужный метод
    }
    
    for (auto shape : shapes) {
        std::cout << "Area: " << shape->area() << std::endl;
    }
}

Вывод:

Circle::draw()
Rectangle::draw()
Area: 78.54
Area: 24

Как это реализуется: VTable

Структура объекта с virtual функциями

class Shape {
    // Скрытый указатель на VTable
    // vptr -> [указатель на VTable]
    
    // членские переменные
public:
    virtual void draw();
    virtual double area();
};

В памяти:

Объект Circle (размер ~16 байт на 64-бит системе):
┌─────────────────┐
│     vptr        │ 8 байт (указатель на VTable)
├─────────────────┤
│    radius       │ 8 байт (double)
└─────────────────┘

VTable для Circle:
┌────────────────────────┐
│   Circle::RTTI         │
├────────────────────────┤
│ Circle::draw()         │ → адрес функции
├────────────────────────┤
│ Circle::area()         │ → адрес функции
├────────────────────────┤
│ Circle::~Circle()      │ → адрес функции
└────────────────────────┘

VTable для Rectangle

VTable для Rectangle:
┌────────────────────────┐
│Rectangle::RTTI         │
├────────────────────────┤
│Rectangle::draw()       │ → адрес функции
├────────────────────────┤
│Rectangle::area()       │ → адрес функции
├────────────────────────┤
│Rectangle::~Rectangle() │ → адрес функции
└────────────────────────┘

Вызов виртуальной функции: под капотом

Shape* shape = new Circle(5);
shape->draw();  // Как это работает?

Сборка (примерно):

mov rax, [rsi]           ; rax = vptr (указатель из объекта)
mov rax, [rax]           ; rax = адрес функции draw() из VTable
call rax                 ; вызываем функцию

В коде это выглядит так:

// Компилятор генерирует:
shape->vptr->draw(shape);  // вызов через VTable

// А не как обычный вызов:
// Shape::draw(shape);  // статический вызов

Стоимость полиморфизма

Память

class NonVirtual {
    int x;  // 4 байта
    // размер: 4 байта
};

class WithVirtual : public NonVirtual {
    // 8 байт (vptr) + 4 байта (x) = 12 байт
};

std::cout << sizeof(NonVirtual) << std::endl;   // 4
std::cout << sizeof(WithVirtual) << std::endl;  // 12

Производительность

// ❌ Медленнее: виртуальный вызов
Shape* shape = new Circle(5);
for (int i = 0; i < 1000000; ++i) {
    shape->draw();  // Dereferencing vptr каждый раз
}

// ✅ Быстрее: прямой вызов
Circle circle(5);
for (int i = 0; i < 1000000; ++i) {
    circle.draw();  // Прямой вызов, может быть inlined
}

Виртуальный вызов медленнее на ~5-10% из-за:

  • Dereferencing vptr (загрузка из памяти)
  • Загрузка адреса функции из VTable
  • CPU не может предсказать ветку заранее (branch prediction failure)

Чистые виртуальные функции (Pure virtual)

class Animal {
public:
    virtual ~Animal() {}  // важно!
    virtual void speak() = 0;  // чистая виртуальная функция
};

class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Woof!" << std::endl;
    }
};

// Animal a;  // ❌ Ошибка: нельзя создать объект абстрактного класса
Dog dog;
dog.speak();  // ✅ OK

Несколько наследования и virtual функции

class Base1 {
public:
    virtual void func() { std::cout << "Base1" << std::endl; }
};

class Base2 {
public:
    virtual void func() { std::cout << "Base2" << std::endl; }
};

class Derived : public Base1, public Base2 {
public:
    void func() override {  // Какую переопределяем обе?
        std::cout << "Derived" << std::endl;
    }
};

Derived d;
Base1* b1 = &d;
Base2* b2 = &d;  // ⚠️ Разные адреса в памяти!
Base1* b1_via_2 = static_cast<Base1*>(b2);  // конвертация указателя

b1->func();       // Derived
b2->func();       // Derived
b1_via_2->func();  // Derived

Переопределение vs скрытие

class Base {
public:
    virtual void func(int x) { std::cout << "Base(int)" << std::endl; }
};

class Derived : public Base {
public:
    void func(double x) {  // ❌ Скрытие, не переопределение!
        std::cout << "Derived(double)" << std::endl;
    }
};

Derived d;
Base* b = &d;
b->func(5);      // Base(int)  — вызывается Base версия!
d.func(5.0);     // Derived(double)
d.func(5);       // ❌ Ошибка: Derived::func(double) скрыла Base::func

Решение: используй override

class Derived : public Base {
public:
    void func(double x) override {  // ✅ Компилятор ошибку
        //error: func marked override but does not override
        std::cout << "Derived(double)" << std::endl;
    }
};

Оптимизация

Devirtualization

Компилятор может оптимизировать известные типы:

Circle c(5);
Shape& shape = c;
shape.draw();  // Компилятор знает, что это Circle
               // Может заменить на прямой вызов Circle::draw()

CRTP (Curiously Recurring Template Pattern)

Альтернатива виртуальным функциям без overhead:

template<typename Derived>
class ShapeBase {
public:
    void draw() {
        static_cast<Derived*>(this)->draw_impl();
    }
};

class Circle : public ShapeBase<Circle> {
public:
    void draw_impl() {
        std::cout << "Circle::draw()" << std::endl;
    }
};

Circle c;
c.draw();  // Нет virtual, компилятор знает точный тип

Итог

Virtual функции позволяют вызов через базовый класс ✅ VTable хранит адреса функций для каждого класса ✅ vptr в каждом объекте указывает на нужную VTable ✅ Стоимость: +8 байт памяти на объект, ~5-10% медленнее ✅ Pure virtual функции делают класс абстрактным ⚠️ Override — используй для проверки на ошибки ⚠️ CRTP — альтернатива, если нужна максимальная производительность