Как работает виртуальность в C++?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как работает виртуальность в 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++:
- Реализуются через VMT (Virtual Method Table)
- Каждый объект хранит vptr (указатель на VMT)
- Вызов идёт через таблицу - это позволяет полиморфизм
- Стоимость минимальна (8 байт памяти, косвенный вызов)
- Нужен virtual деструктор в базовом классе
- Override подсказывает компилятору проверить переопределение
Хорошая практика:
- Всегда используй
virtualв базовом классе - Всегда используй
overrideв производных классах - Всегда делай деструктор
virtualесли есть другие virtual методы - Используй
finalесли хочешь запретить дальнейшее наследование