C++法则14:如果构造函数或析构函数调用了某个虚函数,则我们应该执行与构造函数或析构函数所属类型相对应的虚函数版本。
核心规则
当在构造函数或析构函数中调用虚函数时,调用的版本是当前类(构造函数或析构函数所属的类)定义的版本,而非派生类覆盖(override)的版本。即使当前正在构造或析构的对象是派生类的实例,也是如此。
原因分析
对象构造的顺序
基类的构造函数先于派生类的构造函数执行。当基类构造函数运行时,派生类的成员尚未初始化。
如果此时调用派生类的虚函数,可能会访问未初始化的派生类成员,导致未定义行为(UB)。
对象析构的顺序
派生类的析构函数先于基类的析构函数执行。当基类析构函数运行时,派生类部分已被销毁。
如果此时调用派生类的虚函数,可能会访问已销毁的派生类成员,同样导致UB。
对象类型在构造/析构期间的动态类型
在构造函数中,对象的动态类型是当前正在构造的类(尚未成为派生类)。
在析构函数中,对象的动态类型是当前正在析构的类(派生类部分已不存在)。
因此,虚函数机制会绑定到当前类的版本。
代码示例
#include <iostream>
using namespace std;
class Base {
public:
Base() {
cout << "Base constructor\n";
foo(); // 调用的是 Base::foo(),而非 Derived::foo()
}
virtual ~Base() {
cout << "Base destructor\n";
foo(); // 调用的是 Base::foo(),而非 Derived::foo()
}
virtual void foo() { cout << "Base::foo()\n"; }
};
class Derived : public Base {
public:
Derived() {
cout << "Derived constructor\n";
foo(); // 此时调用 Derived::foo()
}
~Derived() override {
cout << "Derived destructor\n";
foo(); // 此时调用 Derived::foo()
}
void foo() override { cout << "Derived::foo()\n"; }
};
int main() {
Derived d; // 构造和析构测试
return 0;
}
输出:
Base constructor Base::foo() // 基类构造函数中调用的是 Base::foo() Derived constructor Derived::foo() // 派生类构造函数中正常调用 Derived::foo() Derived destructor Derived::foo() // 派生类析构函数中正常调用 Derived::foo() Base destructor Base::foo() // 基类析构函数中调用的是 Base::foo()
关键结论
避免在构造/析构中调用虚函数
如果需要在对象初始化或销毁时执行特定逻辑,可以通过派生类直接调用非虚函数,或传递参数给基类构造函数(依赖注入)。
设计替代方案
使用模板方法模式(Template Method Pattern),将派生类的定制逻辑移到非虚函数中,由基类在安全时机调用。
理解对象生命周期
在构造和析构期间,对象的类型是“不完整”的,虚函数的行为不同于运行时多态。
违反规则的后果
未定义行为:访问未初始化或已销毁的派生类成员。
逻辑错误:预期调用派生类函数,实际调用了基类函数。
遵循此规则能避免潜在的错误,确保代码的健壮性。