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

Как работает виртуальность в C++?

1.7 Middle🔥 261 комментариев
#ООП и проектирование#Язык C++

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

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

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

Как работает виртуальность в C++

Основная идея

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

Это основа полиморфизма в C++.

Простой пример

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

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

class Cat : public Animal {
public:
    void speak() override {
        std::cout << "Meow!\n";
    }
};

int main() {
    Dog dog;
    Cat cat;
    
    Animal* animals[] = {&dog, &cat};
    
    for (auto a : animals) {
        a->speak();  // Вызвать нужный speak() в зависимости от реального типа
    }
    // Output:
    // Woof!
    // Meow!
}

Внутреннее устройство: Virtual Method Table (VMT)

Компилятор использует таблицу виртуальных методов для реализации полиморфизма.

Каждый класс с виртуальными методами имеет:

  • Скрытый указатель на VMT (vptr)
  • Таблица виртуальных методов (содержит указатели на функции)
class Animal {
public:
    virtual void speak() { }
    virtual ~Animal() { }
};

Компилятор генерирует:

Animal object layout:
[vptr -> Animal::VMT]
[data members]

Animal::VMT:
[&Animal::speak]
[&Animal::~Animal]
[...]

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

Animal* ptr = new Dog();
ptr->speak();

Компилятор генерирует:

1. Получить vptr из объекта (ptr->vptr)
2. Найти индекс speak() в VMT (скажем, индекс 0)
3. Получить указатель на функцию из VMT: vptr[0]
4. Вызвать функцию: vptr[0](ptr)  // ptr передаётся как this

В ассемблере (упрощённо):

mov rax, [rdi]       # Получить vptr
call [rax + 0]       # Вызвать первую функцию из VMT

Память для разных классов

class Dog : public Animal {
public:
    std::string name;
    virtual void speak() override { }
};

Layout объекта Dog:

[vptr -> Dog::VMT]    // 8 байт (указатель)
[name (std::string)]  // 24 байта (std::string)

Dog::VMT:
[&Dog::speak]         // Переопределённый метод
[&Dog::~Dog]          // Деструктор
[...другие методы Animal]

Пример с наследованием

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

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

VMT для Base:

Base::VMT:
[0]: &Base::foo
[1]: &Base::bar

VMT для Derived:

Derived::VMT:
[0]: &Derived::foo    // Переопределённый
[1]: &Base::bar       // Унаследованный

Виртуальный деструктор

ОЧЕНЬ ВАЖНО:

// ПЛОХО: нет виртуального деструктора
class Base {
public:
    ~Base() { std::cout << "Base destructor\n"; }
};

class Derived : public Base {
public:
    std::vector<int> data;  // Должен быть удалён
    ~Derived() { std::cout << "Derived destructor\n"; }
};

Base* ptr = new Derived();
delete ptr;  // УТЕЧКА ПАМЯТИ!
// Вызовет только Base::~Base(), не вызовет Derived::~Derived()
// data не будет очищен

ПРАВИЛЬНО:

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

class Derived : public Base {
public:
    std::vector<int> data;
    ~Derived() override { std::cout << "Derived destructor\n"; }
};

Base* ptr = new Derived();
delete ptr;  // OK!
// Вызовет Derived::~Derived(), затем Base::~Base()
// data будет очищен

Стоимость виртуальности

Память:

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

Скорость:

  • Прямой вызов: несколько тактов CPU
  • Виртуальный вызов: несколько тактов + косвенный вызов (cache miss возможен)
  • Обычно разница несущественна (< 5% в реальных приложениях)

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

Используй virtual когда:

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

Не используй virtual когда:

  • Нет наследования
  • Всегда знаешь точный тип объекта
  • Вызываешь через объект, не через указатель
// Виртуальный вызов:
Animal* ptr = new Dog();
ptr->speak();  // Нужна виртуальность

// Неполиморфный вызов (виртуальность не нужна):
Dog dog;
dog.speak();  // Компилятор знает точный тип

Множественное наследование и DMI (Diamond Multiple Inheritance)

class Base {
public:
    virtual void foo() { }
};

class Left : public Base { };
class Right : public Base { };
class Diamond : public Left, public Right { };

Diamond наследует Base дважды! Это создаёт проблемы.

Решение: virtual наследование

class Left : virtual public Base { };
class Right : virtual public Base { };

Теперь Base существует в одном экземпляре.

Pure virtual функции и abstract классы

class Shape {
public:
    virtual void draw() = 0;  // Pure virtual
    virtual ~Shape() { }
};

// Shape невозможно инстанцировать
// Shape s;  // ERROR

class Circle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing circle\n";
    }
};

Circle c;  // OK

Pure virtual функции:

  • Определяют интерфейс
  • Класс с pure virtual нельзя инстанцировать
  • Производные классы должны реализовать

Проверка типа: RTTI (Run-Time Type Information)

Animal* ptr = new Dog();

// dynamic_cast: проверить реальный тип
if (Dog* dog = dynamic_cast<Dog*>(ptr)) {
    std::cout << "It's a dog!\n";
    dog->fetch();  // Метод только у Dog
}

// typeid: получить информацию о типе
if (typeid(*ptr) == typeid(Dog)) {
    std::cout << "It's really a dog\n";
}

Стоимость: RTTI добавляет небольшой overhead (RTTI info в каждом классе).

Итого

Виртуальные функции в C++:

  1. Реализуются через VMT (Virtual Method Table)
  2. Каждый объект хранит vptr (указатель на VMT)
  3. Вызов идёт через таблицу - это позволяет полиморфизм
  4. Стоимость минимальна (8 байт памяти, косвенный вызов)
  5. Нужен virtual деструктор в базовом классе
  6. Override подсказывает компилятору проверить переопределение

Хорошая практика:

  • Всегда используй virtual в базовом классе
  • Всегда используй override в производных классах
  • Всегда делай деструктор virtual если есть другие virtual методы
  • Используй final если хочешь запретить дальнейшее наследование