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

Что такое таблица виртуальных функций?

3.0 Senior🔥 151 комментариев
#ООП и проектирование

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

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

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

Что такое таблица виртуальных функций?

Краткий ответ

Таблица виртуальных функций (vtable, virtual method table) — это скрытая структура, которую компилятор создаёт для каждого класса с виртуальными функциями. Она содержит указатели на реальные функции и позволяет вызывать нужную версию метода во время выполнения (runtime), а не во время компиляции.

Проблема без виртуальных функций

Рассмотрим пример без виртуальных функций:

class Animal {
public:
    void speak() {
        std::cout << "Some sound";
    }
};

class Dog : public Animal {
public:
    void speak() {  // Переопределение, но НЕ виртуальное
        std::cout << "Woof!";
    }
};

int main() {
    Dog dog;
    Animal& animal = dog;
    
    animal.speak();  // Выводит "Some sound", а не "Woof!"
    // Компилятор знает, что animal имеет тип Animal,
    // поэтому вызывает Animal::speak()
}

Результат: Мы потеряли тип Dog и вызвали неправильную функцию.

Решение: виртуальные функции

class Animal {
public:
    virtual void speak() {  // Виртуальная функция
        std::cout << "Some sound";
    }
    virtual ~Animal() {}  // Виртуальный деструктор (важно!)
};

class Dog : public Animal {
public:
    void speak() override {  // override помогает компилятору проверить
        std::cout << "Woof!";
    }
};

int main() {
    Dog dog;
    Animal& animal = dog;
    
    animal.speak();  // Выводит "Woof!" ✓
    // Компилятор генерирует код, который смотрит в vtable
    // и вызывает Dog::speak()
}

Как работает vtable

1. Компилятор создаёт таблицы:

Для каждого класса с виртуальными функциями компилятор создаёт глобальный массив указателей:

// Внутри компилятора для класса Animal:
struct Animal_vtable {
    void (*speak)(Animal* this);  // Указатель на speak
    void (*dtor)(Animal* this);    // Указатель на деструктор
};

// Для Dog:
struct Dog_vtable {
    void (*speak)(Animal* this);  // Указывает на Dog::speak
    void (*dtor)(Animal* this);    // Указывает на Dog::~Dog
};

2. Каждый объект содержит скрытый указатель:

class Animal {
private:
    vtable_ptr vptr;  // Скрытый указатель на vtable (добавляется компилятором)
public:
    virtual void speak() { }
};

3. При вызове виртуальной функции:

Animal& animal = dog;  // animal.vptr указывает на Dog_vtable
animal.speak();

// Компилятор генерирует что-то вроде:
// animal.vptr->speak(&animal);

// Это вызывает Dog::speak(), потому что vptr указывает на Dog_vtable

Пример внутреннего устройства

class Base {
public:
    virtual void foo() { std::cout << "Base::foo\n"; }
    virtual void bar() { std::cout << "Base::bar\n"; }
    virtual ~Base() {}
};

class Derived : public Base {
public:
    void foo() override { std::cout << "Derived::foo\n"; }
    // bar() не переопределяем, используем Base::bar
};

// Компилятор создаёт (концептуально):

// Таблица для Base:
// Base_vtable: [&Base::foo, &Base::bar, &Base::~Base]

// Таблица для Derived:
// Derived_vtable: [&Derived::foo, &Base::bar, &Derived::~Derived]

int main() {
    Base b;
    b.foo();  // Вызывает Base::foo (прямой вызов, не через vtable)
    
    Derived d;
    Base& ref = d;  // ref.vptr = &Derived_vtable
    
    ref.foo();   // Ищет в Derived_vtable[0] → Derived::foo
    ref.bar();   // Ищет в Derived_vtable[1] → Base::bar
    
    delete &ref;  // Вызывает Derived_vtable[2] → Derived::~Derived
}

Механизм dispatch

Статический dispatch (без virtual):

Animal animal;
animal.speak();  // Компилятор ЗНАЕТ, что это Animal
                 // Компилирует как прямой вызов Animal::speak

Динамический dispatch (с virtual):

Animal& ref = ...;  // Компилятор НЕ знает реальный тип!
ref.speak();        // Компилирует как:
                    // ((vtable_of_ref)->speak)(ref);
                    // Вызов во время выполнения (runtime)

Стоимость виртуальных функций

1. Дополнительная память:

Каждый объект с виртуальными функциями содержит скрытый указатель (8 байт на 64-битной системе):

class SimpleClass {};
std::cout << sizeof(SimpleClass);  // 1 байт (пустой класс)

class VirtualClass {
    virtual void foo() { }
};
std::cout << sizeof(VirtualClass);  // 8 байт (добавился vptr)

2. Дополнительная работа при вызове:

Виртуальный вызов:

  • Загрузить vptr из объекта
  • Загрузить указатель функции из vtable
  • Вызвать функцию через указатель

Директный вызов:

  • Сразу прыгнуть на адрес функции

3. Невозможность inline оптимизации:

void foo(Animal& animal) {
    animal.speak();  // Компилятор НЕ может заинлайнить
                     // (не знает реальный тип)
}

Множественное наследование и vtable

class A {
    virtual void foo() { }
};

class B {
    virtual void bar() { }
};

class C : public A, public B {
    void foo() override { }
    void bar() override { }
};

// Объект C содержит ДВА vptr:
// C содержит: [vptr_A, члены A, vptr_B, члены B, члены C]

int main() {
    C c;
    A& a = c;  // a.vptr указывает на первую vtable
    B& b = c;  // b.vptr указывает на вторую vtable
    
    a.foo();  // Использует первую vtable
    b.bar();  // Использует вторую vtable
}

Практические применения

1. Полиморфизм и интерфейсы:

class Shape {
public:
    virtual double area() = 0;  // Чистая виртуальная функция
    virtual ~Shape() {}
};

class Circle : public Shape {
private:
    double radius;
public:
    double area() override { return 3.14 * radius * radius; }
};

class Rectangle : public Shape {
private:
    double width, height;
public:
    double area() override { return width * height; }
};

void printArea(Shape& shape) {
    std::cout << shape.area();  // Вызывает нужную версию!
}

int main() {
    Circle circle;
    Rectangle rect;
    
    printArea(circle);  // Вызовет Circle::area()
    printArea(rect);    // Вызовет Rectangle::area()
}

2. Контейнеры с полиморфизмом:

std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>());
shapes.push_back(std::make_unique<Rectangle>());

for (auto& shape : shapes) {
    std::cout << shape->area();  // Правильная версия для каждого
}

Оптимизации современных компиляторов

Devirtualization:

Когда компилятор может статически определить реальный тип, он убирает vtable:

void foo() {
    Dog dog;  // Компилятор ЗНАЕТ реальный тип
    Animal& animal = dog;
    animal.speak();  // Может быть оптимизировано в Dog::speak()
}

Inlining:

Модерные компиляторы могут заинлайнить виртуальные функции, если знают реальный тип.

Когда использовать virtual

Используй:

  • Когда нужен полиморфизм (разное поведение в зависимости от реального типа)
  • Для базовых классов интерфейсов
  • Всегда помечай деструктор виртуальным в базовых классах!

Не используй:

  • Если не нужен полиморфизм (есть наследование, но нет переопределения методов)
  • В критичном по производительности коде, если можно обойтись без
  • В шаблонах (используй шаблонный метод вместо полиморфизма)

Заключение

Таблица виртуальных функций — это механизм, который:

  • Позволяет вызывать правильную версию метода во время выполнения
  • Требует небольшой дополнительной памяти и времени
  • Является основой объектно-ориентированного программирования в C++

Это одна из самых важных концепций в C++, обеспечивающая полиморфизм и гибкость проектирования!

Что такое таблица виртуальных функций? | PrepBro