我会输入一个例子:
class A
{
public:
virtual ~A(){}
};
class B: public A
{
public:
~B()
{
}
};
int main(void)
{
A * a = new B;
delete a;
return 0;
}
现在在上面的示例中,析构函数将以递归方式从下到上调用。 我的问题是编译器如何做这个MAGIC。
答案 0 :(得分:10)
你的问题中有两个不同的 magic 。第一个是编译器如何为析构函数调用最终的覆盖器,第二个是如何按顺序调用所有其他析构函数。
免责声明:该标准并未强制执行此操作的任何特定方式,它仅强制执行更高级别的操作行为。这些是各种实现共有的实现细节,但不是标准规定的。
编译器如何调度到最终的覆盖?
第一个答案是简单的答案,用于其他virtual
函数的相同动态分派机制用于析构函数。为了刷新它,当编译器看到对任何虚拟的调用时,每个对象都会向每个vptr
存储一个指针(vtable
)(如果有多个继承,则可能有多个)函数,它遵循指针的静态类型的vptr
来找到vtable
,然后使用该表中的指针转发调用。在大多数情况下,调用可以直接调度,在其他情况下(多重继承)它调用一些中间代码( thunk )来修复this
指针以引用的类型该功能的最终覆盖。
编译器如何调用基础析构函数?
破坏对象的过程比在析构函数体内编写的操作需要更多的操作。当编译器为析构函数生成代码时,它会在用户定义的代码之前和之后添加额外的代码。
在调用用户定义的析构函数的第一行之前,编译器会注入一些代码,使对象的类型成为被调用的析构函数的类型。也就是说,在输入~derived
之前,编译器会添加将修改vptr
以引用vtable
的{{1}}的代码,以便有效地实现运行时类型对象变为 derived
(*)。
在用户定义代码的最后一行之后,编译器会调用成员析构函数以及基本析构函数。这是执行禁用动态分派,这意味着它将不再一直到刚刚执行的析构函数。它相当于在析构函数的末尾为对象的每个基础添加derived
(与基数的声明相反)。
使用虚拟继承,事情会变得复杂一些,但总的来说它们遵循这种模式。
编辑(忘了(*)): (*)§12/ 3中的标准要求:
当直接或间接地从构造函数(包括来自数据成员的mem-initializer)或析构函数调用虚函数时,调用所适用的对象是正在构造或销毁的对象,该函数调用是在构造函数或析构函数自己的类或其基础中定义的,但不是在从构造函数或析构函数的类派生的类中重写它的函数,或者在其他一个基类中重写它。最衍生的对象。
该要求意味着对象的运行时类型是此时正在构造/销毁的类的运行时类型,即使正在构造/销毁的原始对象是派生类型。验证此实现的简单测试可以是:
this->~mybase();
答案 1 :(得分:4)
虚拟析构函数的处理方式与任何其他virtual
函数相同。我注意到你已经正确将基类的析构函数设为virtual
。因此,就动态调度而言,它与任何其他virtual
函数没有任何不同。 最派生的类析构函数通过动态调度来调用,但它也会自动调用类 1 类的Base类析构函数。
大多数编译器使用vtable
和vptr
实现此功能,尽管语言规范并未强制要求。可以使用编译器以不同方式执行此操作,而不使用vtable
和vptr
。
无论如何,对大多数编译器来说都是如此,值得知道vtable
是什么。所以vtable
是一个包含类定义的所有虚函数的指针的表,编译器将vptr
作为隐藏指针添加到类中,指向正确的{{1}因此,编译器使用在编译时计算的正确索引到vtable
,以便在运行时调度正确的虚拟函数。
1。 斜体文本取自@ Als的评论。谢谢他。它使事情变得更加清晰。
答案 2 :(得分:2)
与通常的虚函数一样,会有一些实现机制(如vtable指针),它将让编译器根据对象的类型找到首先运行的析构函数。一旦运行了最派生的类析构函数,它将依次运行基类析构函数,依此类推。
答案 3 :(得分:2)
编译器可能使用的(虚拟)析构函数的合适实现将是(在伪代码中)
class Base {
...
virtual void __destruct(bool should_delete);
...
};
void Base::__destruct(bool should_delete)
{
this->__vptr = &Base::vtable; // Base is now the most derived subobject
... your destructor code ...
members::__destruct(false); // if any, in the reverse order of declaration
base_classes::__destruct(false); // if any
if(should_delete)
operator delete(this); // this would call operator delete defined here, or inherited
}
即使您没有定义析构函数,也会定义此函数。在这种情况下,你的代码就是空的。
现在所有派生类都会覆盖(自动)这个虚函数:
class Der : public Base {
...
virtual void __destruct(bool should_delete);
...
};
void Der::__destruct(bool should_delete)
{
this->__vptr = &Der::vtable;
... your destructor code ...
members::__destruct(false);
Base::__destruct(false);
if(should_delete)
operator delete(this);
}
调用delete x
,其中x
是指向类类型的指针,将被翻译为
x->__destruct(true);
和任何其他析构函数调用(由于变量超出范围隐式,显式x.~T()
)将
x.__destruct(false);
这导致
HTH。如果您了解虚函数,这应该是可以理解的。
答案 4 :(得分:1)
由编译器决定如何实现它,通常使用与其他虚拟方法相同的机制完成。换句话说,对于需要虚拟方法调度机制的析构函数没有什么特别之处,这种机制与普通方法使用的机制不同。
答案 5 :(得分:1)
虚拟析构函数在虚拟表中有一个条目,就像其他虚函数一样。当调用析构函数时 - 无论是手动还是自动调用delete
- 都会调用派生最多的版本。析构函数还会自动为析构函数调用其基类,因此与虚拟调度结合使用会导致魔法。
答案 6 :(得分:1)
与其他虚函数不同,当您覆盖虚拟析构函数时,对象的虚拟析构函数另外称为任何继承的虚拟析构函数。
从技术上讲,这可以通过编译器选择的任何方式实现,但几乎所有编译器都通过名为 vtable 的静态内存来实现,这允许函数和析构函数的多态性。对于源代码中的每个类,在编译时为其生成静态常量vtable。当在运行时构造类型为T的对象时,使用隐藏的 vtable指针初始化对象的内存,该指针指向ROM中的T的vtable。在vtable中是一个成员函数指针列表和一个析构函数指针列表。当具有vtable的任何类型的变量超出范围或者使用delete或delete []删除时,对象指向的vtable中的所有析构函数指针都将被调用。 (有些编译器选择只在表中存储派生最多的析构函数指针,然后在每个虚拟析构函数的主体中包含超类的析构函数的隐藏调用(如果存在)。这会产生相同的行为。)
虚拟和非虚拟多重继承需要额外的魔力。假设我正在删除指针p,其中p是基类的类型。我们需要使用 this = p来调用子类的析构函数。但是使用多重继承,p和派生对象的开始可能不一样!必须应用固定偏移量。 vtable中存在一个这样的偏移量,用于继承的每个类,以及一组继承的偏移量。
答案 7 :(得分:0)
当你有一个指向一个对象的指针时,它指向一个内存块,它既包含该对象的数据,又包含一个'vtable指针'。在microsoft编译器中,vtable指针是对象中的第一个数据。在Borland编译器中,它是最后一个。无论哪种方式,它都指向一个vtable,它表示与可以为该对象/类调用的虚方法相对应的函数向量列表。虚析构函数只是函数指针向量列表中的另一个向量。