【问题标题】:How Does Virtual Destructor work in C++虚拟析构函数如何在 C++ 中工作
【发布时间】:2011-12-06 17:00:36
【问题描述】:

我将输入一个示例:

class A
{
public:
virtual ~A(){}
};

class B: public A
{
public:
~B()
{
}

};



int main(void)
{
A * a =  new B;
delete a;
return 0;
}

现在在上面的例子中,析构函数将被递归调用自下而上。 我的问题是编译器如何做到这一点。

【问题讨论】:

  • 我对虚表机制有所了解,但是虚表还有其他虚函数的入口,那么析构函数呢?如果我遗漏或弄乱了某些东西,请纠正我?
  • 如答案所示,虚拟析构函数的处理方式与其他虚拟函数一样,因此通常使用 vtable。有关该魔法的更多信息以及其他方法,请查看this question

标签: c++


【解决方案1】:

您的问题中有两种不同的魔法。第一个是编译器如何调用析构函数的最终覆盖,第二个是它如何依次调用所有其他析构函数。

免责声明:该标准没有规定执行此操作的任何特定方式,它仅规定更高级别的操作的行为是什么。这些是各种实现共有的实现细节,但标准并未强制要求。

编译器如何分派给最终的覆盖器?

第一个答案很简单,与用于其他virtual 函数的动态调度机制相同,用于析构函数。为了刷新它,每个对象都存储一个指向其每个vtables 的指针(vptr)(在多重继承的情况下可以有多个),当编译器看到对任何虚函数的调用时,它遵循指针的静态类型的vptr 找到vtable,然后使用该表中的指针来转发呼叫。在大多数情况下,可以直接分派调用,在其他情况下(多重继承),它会调用一些中间代码(thunk)来修复 this 指针以引用 final 覆盖器的类型 用于该功能。

编译器如何调用基析构函数?

析构对象的过程比你在析构函数体内编写的操作要多。当编译器为析构函数生成代码时,它会在用户定义的代码之前和之后添加额外的代码。

在调用用户定义的析构函数的第一行之前,编译器会注入代码,使对象的类型成为被调用的析构函数的类型。也就是说,在输入~derived 之前,编译器添加了将修改vptr 以引用derivedvtable 的代码,因此实际上,对象的运行时类型变为 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;
}

【讨论】:

  • 简单来说,要求就是:构造函数或析构函数中的this总是指向构造函数或析构函数所属类型的指针。
  • @Als:有点意思,但重要的是要注意并不是指针属于那种类型,而是指针类型随着对象在层次结构中被破坏而改变。跨度>
  • 对您的回答的一个问题。正如您提到的“..它遵循指针的静态类型的vptr来查找vtable..”。不应该是指针的dynamic类型来查找vtable吗?如果它遵循静态类型的 vptr 将是基类的 vptr,或者我想念她什么?
【解决方案2】:

virtual 析构函数的处理方式与任何其他virtual 函数相同。我注意到您正确将基类的析构函数设为virtual。因此,就动态调度而言,它与任何其他virtual 函数没有什么不同。 通过动态调度调用最派生类的析构函数,但它也会自动调用类的基类析构函数1

大多数编译器使用vtablevptr 实现此功能,尽管语言规范没有强制要求。可以有一个编译器以不同的方式执行此操作,而不使用 vtablevptr

无论如何,对于大多数编译器来说都是如此,值得知道vtable 是什么。所以vtable是一个包含类定义的所有虚函数的指针的表,编译器将vptr添加到类中作为指向正确vtable隐藏指针,所以编译器使用vtable 的正确索引,在编译时计算,以便在运行时调度正确的 virtual 函数。

1. 斜体 文本来自@Als 的评论。感谢他。它使事情更清楚。

【讨论】:

  • OP询问虚拟析构函数的magic,答案只解释了虚函数。除了析构函数的行为与所有其他虚函数一样,它还有更多的魔力. 最派生的类析构函数被调用,但它也自动导致调用该类的基类析构函数。答案无法回答 OP 询问的 real Q。
  • @Als:我意识到了这一点,所以我也为此添加了必要的解释。无论如何,感谢您的评论。
  • 您可能还应该添加一个事实,即对于析构函数The most derived class destructor gets called through dynamic dispatch but it also automatically results in calls to Base class destructors of the class,它可以解释析构函数与其他函数不同的完整行为。
  • @Als:是的。好点子。让我添加您评论中的文字。
【解决方案3】:

编译器可能使用的(虚拟)析构函数的合适实现是(在伪代码中)

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。如果您了解虚函数,这应该是可以理解的。

【讨论】:

  • 我同意这一点,除了 if(should_delete) “删除这个”。真正的 C++ 析构函数没有这样的特性,也不应该。相反,当调用 delete 时,它​​们会调用析构函数,然后删除该内存块。没有递归堆释放。
  • @VoidStar:那么您必须不同意 C++ 标准本身及其 g++ 实现。看看它是如何工作的:ideone.com/F9gjv
  • @jpalecek:ideone 中的那个演示没有展示任何东西,我的标准规定作为 delete-expression §5.3.5/6 的描述的一部分删除-expression 将为要删除的对象或数组的元素调用析构函数(如果有)。 [...] §5.3.5/7 删除表达式将调用释放函数。析构函数调用deleter,但是delete-expression调用destructor然后deallocator跨度>
  • 顺便说一句,示例中没有“递归堆释放”。
  • 对于不应该出现在答案中的一件事,在另一端,您只缺少在进入析构函数之前将对象的动态类型设置为的位被调用的析构函数。但总体而言,这是一个很好的答案,可以解决最被忽视的(破坏)+1
【解决方案4】:

与通常的虚函数一样,会有一些实现机制(如 vtable 指针),让编译器根据对象的类型找到首先运行的析构函数。一旦运行了最派生类的析构函数,它将依次运行基类析构函数等等。

【讨论】:

    【解决方案5】:

    如何实现它取决于编译器,通常它使用与其他虚拟方法相同的机制来完成。换句话说,析构函数没有什么特别之处,它需要一个不同于普通方法使用的虚拟方法分派机制。

    【讨论】:

    • 除了调用所有继承的析构函数,因为只有最派生的覆盖被调用用于普通函数。这需要在 vtable 中进行一些特殊处理,或者在析构函数的主体中隐藏对超级析构函数的调用。
    • @VoidStar 嗯,这是一个务实的语言决定。对于普通的虚函数,函数可以选择调用或不调用基类函数。对于析构函数,语言指定这将始终发生,并且对基类析构函数的调用是隐式的。实现这一点的底层机制通常是相同的。
    【解决方案6】:

    和其他虚函数一样,虚析构函数在虚表中有一个条目。当析构函数被调用时——无论是手动调用还是自动调用delete——都会调用最衍生的版本。析构函数也会自动调用其基类的析构函数,因此与虚拟调度相结合是导致魔法的原因。

    【讨论】:

    • 调用所有继承的析构函数,而不仅仅是最派生的。
    • @VoidStar:是的,这就是我的回答所说的那么这个评论是关于什么的?
    • 哎呀,我把“最衍生版本被调用”归零。抱歉,我一定是浏览的太快了。
    【解决方案7】:

    与其他虚函数不同,当您重写虚析构函数时,对象的虚析构函数会被调用除了任何继承的虚析构函数。

    从技术上讲,这可以通过编译器选择的任何方式来实现,但几乎所有编译器都是通过称为 vtable 的静态内存实现的,它允许函数和析构函数的多态性。对于源代码中的每个类,都会在编译时为其生成一个静态常量 vtable。当一个类型 T 的对象在运行时被构造时,对象的内存被一个隐藏的 vtable 指针 初始化,它指向 T 在 ROM 中的 vtable。 vtable 内部是一个成员函数指针列表和一个析构函数指针列表。当具有 vtable 的任何类型的变量超出范围或被 delete 或 delete[] 删除时,对象指向的 vtable 中的所有析构函数指针都会被调用。 (一些编译器选择只在表中存储最派生的析构函数指针,然后在每个虚拟析构函数的主体中包含对超类析构函数的隐藏调用(如果存在)。这会导致等效行为。)

    虚拟和非虚拟多重继承需要额外的魔法。假设我正在删除一个指针 p,其中 p 是基类的类型。我们需要用 this=p 调用子类的析构函数。但是使用多重继承,p和派生对象的开头可能不一样!有一个必须应用的固定偏移量。对于每个被继承的类,在 vtable 中存储了一个这样的偏移量,以及一组继承的偏移量。

    【讨论】:

    • +1,但 vtable 中存储了一个这样的偏移量:大多数编译器将使用 thunktrampoline函数 修复this 指针。也就是说,如果this 不需要偏移,vtable 中的条目将指向最终覆盖器,或者指向将执行偏移并转发调用的 thunk。在vtable 中存储偏移量的优势在于,您只需在需要时为偏移量付费(存储了偏移量,编译器必须添加它,即使在大多数情况下它是 0)
    【解决方案8】:

    当你有一个指向对象的指针时,它指向一个内存块,其中包含该对象的数据和一个“vtable 指针”。在 microsoft 编译器中,vtable 指针是对象中的第一条数据。在 Borland 编译器中,它是最后一个。无论哪种方式,它都指向一个 vtable,该 vtable 表示与可以为该对象/类调用的虚拟方法相对应的函数向量列表。虚拟析构函数只是该函数指针向量列表中的另一个向量。

    【讨论】:

      猜你喜欢
      • 2013-08-06
      • 1970-01-01
      • 2011-08-12
      • 2019-06-26
      • 1970-01-01
      • 2012-04-18
      • 1970-01-01
      • 2018-01-10
      • 2017-05-31
      相关资源
      最近更新 更多