您的问题中有两种不同的魔法。第一个是编译器如何调用析构函数的最终覆盖,第二个是它如何依次调用所有其他析构函数。
免责声明:该标准没有规定执行此操作的任何特定方式,它仅规定更高级别的操作的行为是什么。这些是各种实现共有的实现细节,但标准并未强制要求。
编译器如何分派给最终的覆盖器?
第一个答案很简单,与用于其他virtual 函数的动态调度机制相同,用于析构函数。为了刷新它,每个对象都存储一个指向其每个vtables 的指针(vptr)(在多重继承的情况下可以有多个),当编译器看到对任何虚函数的调用时,它遵循指针的静态类型的vptr 找到vtable,然后使用该表中的指针来转发呼叫。在大多数情况下,可以直接分派调用,在其他情况下(多重继承),它会调用一些中间代码(thunk)来修复 this 指针以引用 final 覆盖器的类型 用于该功能。
编译器如何调用基析构函数?
析构对象的过程比你在析构函数体内编写的操作要多。当编译器为析构函数生成代码时,它会在用户定义的代码之前和之后添加额外的代码。
在调用用户定义的析构函数的第一行之前,编译器会注入代码,使对象的类型成为被调用的析构函数的类型。也就是说,在输入~derived 之前,编译器添加了将修改vptr 以引用derived 的vtable 的代码,因此实际上,对象的运行时类型变为 em> derived (*).
在用户定义代码的最后一行之后,编译器会注入对成员析构函数和基析构函数的调用。这是通过禁用动态调度执行的,这意味着它将不再一直到刚刚执行的析构函数。它相当于在析构函数的末尾为对象的每个基添加this->~mybase();(与基的声明顺序相反)。
使用虚拟继承,事情会变得有点复杂,但总的来说它们遵循这种模式。
编辑(忘记了(*)):
(*) §12/3 中的标准规定:
当从构造函数(包括从数据成员的mem-initializer)或从析构函数直接或间接调用虚函数时,调用应用的对象是正在构造或销毁的对象,函数被调用的是在构造函数或析构函数自己的类或其基类之一中定义的函数,但不是在从构造函数或析构函数的类派生的类中覆盖它的函数,或在其他基类之一中覆盖它的函数大多数派生对象。
该要求意味着对象的运行时类型是此时正在构造/析构的类的类型,即使正在构造/析构的原始对象是派生类型。验证此实现的简单测试可以是:
struct base {
virtual ~base() { f(); }
virtual void f() { std::cout << "base"; }
};
struct derived : base {
void f() { std::cout << "derived"; }
};
int main() {
base * p = new derived;
delete p;
}