条款9(构造/析构):绝不在构造和析构过程中调用virtual函数(Never call virtual functions during construction or destruction)
场景描述:考虑一个用于表示“股票交易(Transaction)”的类
- 需求1:股票交易的类型有两种(即存在两个派生类),分别为“买入交易BuyTransaction”和“卖出交易SellTransaction”。
- 需求2:每笔交易都需要进行日志记录(即每创建一个交易对象,就需要在日志记录中进行适当的记录)。
实现1(不可行,向下调用)
设计思路:在基类构造函数中,调用日志记录函数(虚函数)
程序分析:企图通过虚函数实现运行时多态,从而达到“每当Transaction继承体系上的对象被创建时,都会调用(派生类)适当版本的日志记录函数logTransaction”(实际上并不会)。



运行结果:执行“BuyTransaction b;”(创建“买入交易”对象)
- Step1:进入“派生类”的默认构造函数前,初始化列表中会调用“基类”的默认构造函数(即便没有显式声明,编译器也会自动调用)
- Step1.2:执行“基类”的默认构造函数
- 打印输出"Transaction base class ctor. called…\\n"
- Step1.3:执行“基类”的纯虚函数(重点!!!)
- 打印输出"Transaction base class logTransaction() called…\\n"
- Step2:进入“派生类”的默认构造函数
- 打印输出"BuyTransaction derived class ctor. called…\\n"
- 观察:被调用的logTransaction(虚函数)是Transaction(基类)内的版本,不是BuyTransaction(派生类)内的版本,即使目前即将创建的对象类型是BuyTransaction。(不符合预期)

注1(对象构造顺序):当构造派生类对象时,基类构造函数早于派生类构造函数执行(即基类成分早于派生类自身成分构造)。因此,当基类构造函数运行时,派生类的成员变量尚未被初始化。此时,如果允许虚函数的调用下降至派生类,则被调用的函数几乎必然会访问那些尚未初始化的成员变量,从而导致未定义行为(undefined behavior)。因此,C++从语言层面上直接禁止了这种行为,而不是把风险交给程序员自行承担。
注2(对象类型):在派生类对象的基类构造阶段,对象的动态类型被视为基类类型,而非派生类类型。这一规则被C++的每一个次语言(见“条款1”)一致遵循,其设计具有合理性:在该阶段,对象中属于派生类的成员尚未完成初始化。因此,语言规则将其视为尚不存在(最安全的做法)。在派生类构造函数开始执行前,对象不会成为一个派生类对象。不仅虚函数会被编译器解析到基类版本,所有运行期类型信息(Runtime Type Information, RTTI)机制也都遵循这一规则。例如,dynamic_cast和typeid,在此阶段都会将对象视为基类类型。
注3(析构函数):当析构派生类对象时,派生类析构函数早于基类析构函数执行(即派生类自身成分早于基类成分析构)。(道理相同)
- 一旦派生类析构函数开始执行,对象内的派生类成员变量便呈现未定义值。因此,在逻辑上,对象已经不再被视为一个“完整的派生类对象”,因此不应依赖其完整的派生类行为。
- 进入基类析构函数后,派生类部分已经被销毁,此时对象只剩下基类子对象。从语言规则角度看,该对象已经被视为一个基类对象,而C++的任何部分包括virtual函数、dynamic_cast等等也就那么看待它。
注4(编译器警告):
- 当构造函数直接调用纯虚函数时,除非它被定义(不太有希望,但是有可能,见“条款34”),否则程序链接错误(链接器找不到必要的实现代码)。
- 当构造函数直接调用普通虚函数时,这很明显而且容易看出,某些编译器会为此发出一个警告信息(某些则否)。
实现2(可行,向上传递)
在派生类构造函数中,(利用成员初值列)传递必要信息给基类构造函数;在基类构造函数中,调用日志记录记录函数(非虚函数),并传递接收到的必要信息。


设计思路:由于无法在基类构造函数中使用虚函数向下调用,可以借由“让派生类将必要的构造信息向上传递给基类构造函数”替换之而加以补偿。
程序分析:
- 函数调用传递与值传递:使用“辅助函数来创建一个值”传递给基类构造函数,通常比“具体数据(字符串、整数等)”传递给基类构造函数具有更好的可读性(函数名createLogString能够清晰表达创建日志记录的意图)。
- 静态与非静态:通过将函数设置为静态,避免基类构造期间使用派生类尚未初始化的数据成员。
运行结果:BuyTransaction b("Log BuyTransaction…");

小结
请记住:在对象构造(创建)和析构(销毁)期间,确保构造函数和析构函数都没有调用虚函数,而它们调用的所有函数也都服从同一约束。
- 基类构造期间,虚函数的调用绝不会下降到派生类阶层(比起当前执行构造函数和析构函数的那层)。取而代之的是,对象的作为就像隶属基类类型一样。
- 基类构造期间,虚函数不是虚函数。
知识补充
在派生类对象的基类构造阶段,C++会把该对象当成“基类对象”看待,而不是派生类对象。这种规则不仅影响虚函数调用,也影响RTTI机制,包括dynamic_cast和typeid。
示例1(dynamic_cast):
- 程序分析:

- 运行结果:在Base()构造函数执行时,Derived部分还没构造,对象 当前动态类型为Base。因此,dynamic_cast<Derived*>(this)会失败,返回nullptr。即使最终创建的是Derived对象,也不行。

示例2(typeid):
- 程序分析:

- 运行结果:
