我会输入一个例子:
class A
{
public:
virtual ~A(){}
};
class B: public A
{
public:
~B()
{
}
};
int main(void)
{
A * a = new B;
delete a;
return 0;
}
Run Code Online (Sandbox Code Playgroud)
现在在上面的例子中,析构函数将以递归方式从下到上调用.我的问题是编译器如何做这个MAGIC.
Dav*_*eas 10
你的问题中有两种不同的魔法.第一个是编译器如何为析构函数调用最终的覆盖器,第二个是如何按顺序调用所有其他析构函数.
免责声明:该标准并未强制执行此操作的任何特定方式,它仅强制执行更高级别的操作行为.这些是各种实现共有的实现细节,但不是标准强制要求的.
编译器如何调度到最终的覆盖?
第一个答案是简单的答案,用于其他virtual函数的相同动态调度机制用于析构函数.为了刷新它,每个对象都存储一个指针(vptr)到它vtable的每一个(在多重继承的情况下可以有多个),当编译器看到对任何虚函数的调用时,它遵循vptr静态类型找到该指针vtable然后使用该表中的指针转发该调用的指针.在大多数情况下,调用可以直接调度,在其他情况下(多重继承)它调用一些中间代码(thunk)来修复this指针以引用该函数的最终覆盖类型.
然后编译器如何调用基础析构函数?
破坏对象的过程比在析构函数体内编写的操作要多.当编译器为析构函数生成代码时,它会在用户定义的代码之前和之后添加额外的代码.
在调用用户定义的析构函数的第一行之前,编译器会注入代码,该代码将使对象的类型成为被调用的析构函数的类型.也就是说,前右~derived输入,编译器添加代码将修改vptr来指代vtable的derived,这样有效,该对象的运行时类型变为 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;
}
Run Code Online (Sandbox Code Playgroud)
虚拟析构函数的处理方式与任何其他函数相同virtual。我注意到您已正确地将基类的析构函数设置为virtual. 因此,就virtual动态调度而言,它与任何其他函数没有什么不同。最派生的类析构函数通过动态分派进行调用,但它也会自动导致对类1的基类析构函数的调用。
vtable大多数编译器使用和实现此功能vptr,尽管语言规范没有强制要求。可以有一个编译器以不同的方式执行此操作,而不使用vtableand vptr。
无论如何,正如大多数编译器一样,了解它是什么是值得的vtable。因此vtable,一个表包含了类定义的所有虚函数的指针,并且编译器将其作为指向正确的隐藏指针vptr添加到类中,因此编译器使用在编译时计算的正确索引来分派在运行时正确的虚函数。vtablevtable
1.斜体文本取自@Als的评论。感谢他。它让事情变得更加清晰。