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

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

1.7 Middle🔥 151 комментариев
#ООП и проектирование#Структуры данных и алгоритмы#Язык C++

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

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

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

vtable и vptr: механизм виртуальных функций

vtable (virtual method table) и vptr (virtual pointer) — это низкоуровневые механизмы реализации полиморфизма.

Что такое vtable и vptr

  • vtable — таблица указателей на виртуальные функции класса
  • vptr — скрытый указатель в каждом объекте, указывающий на vtable
  • Каждый класс с виртуальными функциями имеет свою vtable

Как работает механизм

#include <iostream>

class Base {
public:
    virtual void method1() { std::cout << "Base::method1" << std::endl; }
    virtual void method2() { std::cout << "Base::method2" << std::endl; }
    virtual ~Base() {}
};

class Derived : public Base {
public:
    void method1() override { std::cout << "Derived::method1" << std::endl; }
    void method2() override { std::cout << "Derived::method2" << std::endl; }
};

int main() {
    Derived obj;
    Base* ptr = &obj;
    
    ptr->method1();  // Вызовет Derived::method1
    ptr->method2();  // Вызовет Derived::method2
    
    return 0;
}

Внутреннее устройство:

  1. Создание объекта Derived: объект содержит скрытый vptr
  2. Инициализация vptr: конструктор устанавливает vptr на vtable Derived
  3. Вызов виртуальной функции:
    • Получить vptr из объекта
    • Найти адрес функции в vtable (по индексу)
    • Вызвать функцию

Размер объекта с vptr

#include <iostream>

class WithoutVirtual {
    int x;  // 4 байта
};

class WithVirtual {
public:
    virtual void foo() {}
    int x;  // 4 байта
};

int main() {
    std::cout << "Without virtual: " << sizeof(WithoutVirtual) << std::endl;
    // 4 байта (только int)
    
    std::cout << "With virtual: " << sizeof(WithVirtual) << std::endl;
    // 16 байт (8 байт vptr + 4 байта int + padding)
    
    return 0;
}

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

#include <iostream>

class A {
public:
    virtual void methodA() { std::cout << "A" << std::endl; }
    virtual ~A() {}
};

class B {
public:
    virtual void methodB() { std::cout << "B" << std::endl; }
    virtual ~B() {}
};

class C : public A, public B {
public:
    void methodA() override { std::cout << "C::A" << std::endl; }
    void methodB() override { std::cout << "C::B" << std::endl; }
};

int main() {
    C obj;
    
    A* ptrA = &obj;
    B* ptrB = &obj;
    
    // Оба указателя указывают на один объект,
    // но на разные vtable!
    ptrA->methodA();  // C::A
    ptrB->methodB();  // C::B
    
    return 0;
}

Объект содержит несколько vptr (по одному для каждого базового класса).

Трассировка вызова

class Animal {
public:
    virtual void speak() { std::cout << "Animal" << std::endl; }
    virtual ~Animal() {}
};

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

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

int main() {
    Dog dog;
    Cat cat;
    
    Animal* animals[] = { &dog, &cat };
    
    for (int i = 0; i < 2; ++i) {
        // 1. Получить vptr из animals[i]
        // 2. Получить адрес speak() из vtable
        // 3. Вызвать speak()
        animals[i]->speak();
    }
    return 0;
}

Как компилятор генерирует код

// На C++
ptr->method1();

// Компилятор генерирует примерно:
mov rax, qword ptr [ptr]      // Получить адрес объекта
mov rbx, qword ptr [rax]      // Получить vptr
mov rcx, qword ptr [rbx]      // Получить функцию из vtable
call rcx                        // Вызвать функцию

Отключение виртуального вызова

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

class Derived : public Base {
public:
    void func() override { std::cout << "Derived" << std::endl; }
};

int main() {
    Derived obj;
    
    Base* ptr = &obj;
    ptr->func();           // Через vtable -> Derived
    
    obj.Derived::func();   // Прямой вызов -> Derived
    obj.Base::func();      // Прямой вызов -> Base
    
    return 0;
}

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

Стоимость виртуального вызова:

  • Дополнительное разыменование указателя (2-3 такта)
  • Нарушение предсказания ветвлений процессора
  • Отсутствие инлайнинга оптимизатором

Ключевые моменты

  1. vptr добавляется только к классам с виртуальными функциями
  2. Один vptr на объект (несколько при множественном наследовании)
  3. vtable — глобальна, общая для всех объектов класса
  4. Выбор функции происходит в runtime, а не в compile-time
  5. Производительность немного хуже, чем прямые вызовы

Итог: vtable/vptr обеспечивают полиморфизм путём косвенного вызова функций через таблицы указателей, что позволяет одному указателю вызывать разные реализации в зависимости от типа объекта.

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