
图中派发目标随构造析构阶段变化,完整派生对象的规则不能提前套到基类阶段。
基类构造函数调用一个虚函数,创建的明明是派生类对象,为什么没有进入派生类覆盖版本?这并不意味着虚函数失效,而是构造和析构期间,语言对当前对象的虚派发有专门规则。先看执行处于哪一阶段,再判断调用目标。
完整对象的直觉只适用于相应阶段
对象完成构造之后,通过合法的基类引用或指针调用虚函数,通常会使用动态类型对应的最终覆盖版本。但在基类构造函数执行时,派生部分还没有完成建立,不能把完整派生对象的通常调用规则原样搬过来。
当虚函数调用作用于当前正在构造或析构的对象时,派发按当前构造析构阶段所属的类确定,不会继续调用更派生层的覆盖版本。这里强调当前对象;不能据此声称构造函数里对任何其他完整对象的虚调用都被关闭。
这一限制也适用于经由辅助函数间接发生的相关虚调用,不能靠把那行调用挪进一个普通成员函数,就自动提前获得派生层行为。分析必须追踪调用作用于哪个对象。
用构造、使用、析构连起来看
下面的示例让基类和派生类分别输出自己的标记:
#include <iostream>
struct Base {
Base() { f(); }
virtual void f() { std::cout << "B "; }
virtual ~Base() { f(); }
};
struct Derived : Base {
Derived() { f(); }
void f() override { std::cout << "D "; }
~Derived() override { f(); }
};
int main() {
{ Derived d; d.f(); }
std::cout << std::endl;
}
依次观察到 B、D、D、D、B。第一项来自基类构造阶段,第二项来自派生构造阶段,第三项是构造完成后的普通调用。离开作用域后,先运行派生析构,再运行基类析构,所以最后两个标记分别是 D 和 B。

不要把结果解释成内存猜谜
一种常见解释是“虚表指针被反复改写”。这能帮助理解某些编译器的实现,但题目的可移植结论来自语言规则,不依赖你观察到几张虚表、具体指针放在哪里,或者目标平台怎样组织对象布局。
即便优化器把某些调用直接化,合法程序的可观察行为也应符合规则。反过来,不能因为反汇编中没看到间接跳转,就声称程序从来没有虚函数语义。语义与实现手段应分别说明。
这里也没有通过基类指针删除派生对象的非法示例。基类析构函数声明为 virtual,是为了让相关多态销毁遵守正确的析构链;这与析构函数内部调用 f 时选择哪个版本,是两个不同问题。
纯虚函数不是等待派生类救场
如果基类把某个方法声明为纯虚,不能在基类构造或析构阶段通过虚调用期待派生实现来补上。此类纯虚调用有严格风险,相关情形属于未定义行为,不能拿某个编译器打印信息或某次崩溃作为所有实现的标准结果。
即使纯虚函数可以存在独立定义,也不意味着任意构造期虚调用就合法。显式限定名称调用与虚派发还要区分,不能把一种语法下的行为推到另一种语法。基础回答只需守住边界,不必现场运行未定义行为来“证明”。
初始化依赖别靠过早回调
如果基类初始化必须调用子类定制逻辑,很容易形成时机问题:派生成员尚未准备好,基类又想使用它们。可以重新设计构造参数,把必要信息显式传入;或者在对象完整建立后,通过明确的工厂与初始化流程完成相应步骤。
后者也要保证未初始化对象不会提前逃逸,且初始化失败有清楚的资源管理。把构造里的一次回调挪到别处,并不自动解决所有生命周期问题。设计目标是让对象何时可用能够被检查。
面试时先给出阶段规则,再用标记序列解释构造和析构,最后区分语言语义、编译器实现与纯虚调用风险。若基类构造期间把 this 传给一个辅助函数,再由它调用虚函数,你会怎样判断派发目标?