← Назад к вопросам
Какие знаешь особенности вызова виртуальной функции из конструктора?
2.2 Middle🔥 121 комментариев
#ООП и проектирование#Язык C++
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Какие знаешь особенности вызова виртуальной функции из конструктора
Это классический вопрос на C++ собеседованиях и часто является причиной трудноуловимых ошибок. Ключевая особенность — во время выполнения конструктора базового класса виртуальные функции вызывают версию базового класса, а не производного, даже если объект является экземпляром производного класса.
Основная проблема: динамический тип vs статический тип
class Base {
public:
Base() {
// Во время конструктора Base, vtable указывает на Base
doSomething(); // Вызывает Base::doSomething()
}
virtual void doSomething() {
std::cout << "Base::doSomething()\n";
}
virtual ~Base() = default;
};
class Derived : public Base {
public:
Derived() {
// Во время конструктора Derived, уже можно вызвать Derived::doSomething()
doSomething(); // Вызывает Derived::doSomething()
}
void doSomething() override {
std::cout << "Derived::doSomething()\n";
}
};
int main() {
// Вывод:
// Base::doSomething() <- из конструктора Base
// Derived::doSomething() <- из конструктора Derived
Derived d;
// После конструктора работает нормально
d.doSomething(); // Вывод: Derived::doSomething()
}
Почему это происходит: порядок конструирования
1. RTTI (Run-Time Type Information) инициализация
Язык C++ инициализирует vtable поэтапно:
class Base {
public:
Base() {
// На этом этапе:
// vptr (virtual pointer) указывает на vtable BASE
// Тип объекта считается Base (в RTTI)
virtualFunc(); // Base::virtualFunc()
}
virtual void virtualFunc() { }
};
class Derived : public Base {
public:
Derived() : Base() { // Сначала конструктор Base
// После конструктора Base:
// vptr теперь указывает на vtable DERIVED
virtualFunc(); // Derived::virtualFunc()
}
void virtualFunc() override { }
};
2. Порядок инициализации членов
class Complex : public Base {
public:
Complex() : Base(), member(nullptr) {
// 1. Конструктор Base() <- virtualFunc() зовёт Base версию
// 2. Инициализируются member переменные (member = nullptr)
// 3. Конструктор Complex
// 4. Теперь vptr указывает на Complex::vtable
virtualFunc(); // Complex::virtualFunc()
}
int* member;
void virtualFunc() override {
if (member == nullptr) { // МОЖЕТ БЫТЬ ОШИБКА!
// ...
}
}
};
Практическая проблема: доступ к неинициализированным членам
class Observer : public Base {
public:
Observer() : data_(0) {
// Base() уже вызвал virtualFunc()
// data_ ещё не инициализирован!
}
void virtualFunc() override {
// Опасно! data_ может быть случайным значением
processData(data_); // UNDEFINED BEHAVIOR
}
private:
int data_;
};
Observer o; // Вывод: обработка random значения data_
Решение 1: Избегать виртуальных вызовов в конструкторе
class Base {
public:
Base() {
// Не вызываем виртуальные функции
// init() будет вызвана явно после конструктора
}
virtual void initialize() { }
virtual ~Base() = default;
};
class Derived : public Base {
public:
Derived() : value_(0) { }
void initialize() override {
std::cout << "Value: " << value_ << "\n";
}
private:
int value_;
};
int main() {
Derived d;
d.initialize(); // Вызывается явно ПОСЛЕ конструктора
}
Решение 2: Template Method Pattern
class Base {
public:
Base() {
// Вызываем нелинейную функцию с известным поведением
doInitialization();
}
// Финальная функция в Base, вызывает pure virtual
void doInitialization() {
setupResources();
configureSettings();
}
// Pure virtual — должна быть реализована
virtual void configureSettings() = 0;
private:
void setupResources() {
// Базовая инициализация
}
};
class Derived : public Base {
public:
Derived() : data_(0) { }
void configureSettings() override {
// Вызывается из doInitialization()
// Но data_ ещё не инициализирована!
// ОШИБКА остаётся
}
private:
int data_;
};
Решение 3: Двухэтапная инициализация (Two-Phase Init)
class Base {
public:
Base() : initialized_(false) { }
virtual ~Base() = default;
void initialize() {
if (!initialized_) {
doInitialize();
initialized_ = true;
}
}
virtual void doInitialize() = 0;
protected:
bool isInitialized() const { return initialized_; }
private:
bool initialized_;
};
class Derived : public Base {
public:
Derived() : data_(0) { }
void doInitialize() override {
// Теперь data_ полностью инициализирован
processData(data_);
}
private:
int data_;
};
int main() {
{
Derived d;
d.initialize(); // Безопасный вызов
}
}
Решение 4: Factory Pattern
class Base {
protected:
Base() { }
public:
virtual ~Base() = default;
virtual void configure() = 0;
};
class Derived : public Base {
private:
Derived() : value_(0) { }
friend class Factory;
public:
void configure() override {
// value_ полностью инициализирован
}
private:
int value_;
};
class Factory {
public:
static std::unique_ptr<Base> create() {
auto obj = std::unique_ptr<Base>(new Derived());
obj->configure(); // Вызов ПОСЛЕ полного конструирования
return obj;
}
};
int main() {
auto base = Factory::create();
}
Проверка в деструкторе
То же самое происходит и в деструкторе, но в обратном порядке:
class Base {
public:
virtual ~Base() {
doCleanup(); // Вызывает Base версию!
}
virtual void doCleanup() { }
};
class Derived : public Base {
public:
~Derived() override {
// data_ ещё живой, но деструктор Base
// вызовет Base::doCleanup()
}
private:
int data_;
};
Лучшие практики
- Не вызывай виртуальные функции в конструкторе
- Используй двухэтапную инициализацию для сложных объектов
- Помечай функции как
finalесли они не должны перекрываться - Используй
noexceptв конструкторах для гарантированной безопасности - Тестируй иерархии классов на этот баг
// Хороший паттерн
class SafeBase {
public:
SafeBase() = default;
// Инициализация вне конструктора
virtual void init() { }
virtual ~SafeBase() = default;
};
class SafeDerived : public SafeBase {
public:
SafeDerived() : initialized_(false) { }
void init() override {
// Всё инициализировано к этому моменту
initialized_ = true;
}
private:
bool initialized_;
};
Эта особенность одна из самых трудноуловимых в C++ и может привести к серьёзным bagам, поэтому её знание очень ценится на собеседованиях.