Как в С++ реализуется полиморфизм?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Ответ: Полиморфизм в 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 — альтернатива, если нужна максимальная производительность