【问题标题】:How "virtual" impact on destructor in C++?C ++中的析构函数如何“虚拟”影响?
【发布时间】:2018-01-10 10:33:25
【问题描述】:

官方解释的虚函数是:

虚函数是您希望在派生类中重新定义的成员函数。当您使用指针或对基类的引用来引用派生类对象时,您可以为该对象调用虚函数并执行派生类的函数版本。


请先看代码:

#include<iostream>
using namespace std;

class A
{
public:
    A(){cout << "A()" << endl;}
    ~A(){cout << "~A()" << endl;}
};

class B:public A
{
public:
    B(): A(){cout << "B()" << endl;}
    ~B(){cout << "~B()" << endl;}
};

int main()
{
    A * pt = new B;
    delete pt;
}

输出是:

A()
B()
~A()

我的问题是:

  1. 基类的析构函数不能被派生类继承,为什么我们要把基类的析构函数设为虚函数?
  2. 对于上面的代码,我知道这会导致问题(这里没有调用 B 类的析构函数)。我从 google 和 stackoverflow 搜索了很多文章或问题,所有这些都告诉我 base 的析构函数应该是虚拟的,但是析构函数的“虚拟”如何工作?我的意思是,有/没有“虚拟”析构函数的核心代码级别有什么区别?

【问题讨论】:

  • “基类的析构函数不能被派生类继承” - 顺便说一句,你为什么这么认为?
  • 如果您的派生类析构函数是虚拟的,那么对象将按顺序被解构(首先是派生对象,然后是基类)。如果你的派生类析构函数不是虚拟的,那么只有基类对象会被删除
  • 你熟悉 vtables 吗? stackoverflow.com/a/99341/951890
  • @Hariom Singh,您所说的只是告诉我们这种现象,但没有解释原因。这就是我发布问题的原因。
  • 在这种情况下,析构函数中的virtualvirtual 函数没有什么不同:它只是确保调用了实际对象实例的析构函数。什么让你感到困惑?

标签: c++ undefined-behavior delete-operator vtable virtual-destructor


【解决方案1】:

如果A 的析构函数不是虚拟的,则delete pt; 会导致undefined behaviour

A的析构函数设为虚拟的原因是为了能够使用delete pt;删除B对象。

这样做的基本原理是,当编译器看到delete pt; 时,一般来说,它无法知道pt 是否指向B 对象,因为直到运行才能做出该决定-时间。因此,您需要查找对象(在本例中为 vtable)的一些运行时属性,以找出要调用的正确析构函数。

其他一些 cmets/answers 表明您的原始代码的定义行为是不调用 B 的析构函数或其他东西。然而这是错误的。您只是看到了未定义行为的症状,可能是这种行为或其他任何行为。

【讨论】:

    【解决方案2】:

    如果析构函数被标记为虚拟,那么当您调用delete 时,您分配的对象的动态类型的析构函数将被调用。在您的示例中,堆上对象的静态类型是 A,而动态类型是 B。

    由于您没有将析构函数标记为虚拟,因此不会有运行时分派,并且会调用 A 的析构函数。这是错误的,应该修复。如果您打算以多态方式使用一个类,请确保它的析构函数是虚拟的,以便派生类的实例可以释放它们获得的任何资源。

    【讨论】:

      【解决方案3】:

      想象一下 vtable 是如何实现的。

      具有虚方法的类将指向函数指针表的指针作为其第一个元素。

      virtual on 一个方法意味着它的虚函数表中有一个条目。

      对于一个方法,继承的类在覆盖时替换条目。

      对于析构函数,条目实际上是“如何在此对象上调用删除”。所有下降的类都会自动覆盖它。从概念上讲,它将delete base_ptr 的调用转换为if (base_ptr) base_ptr-&gt;vtable-&gt;deleter(base_ptr);

      那么,派生的删除器实际上是(几乎)delete static_cast&lt;derived*&gt;(ptr);,这与通常的删除调用一样,它按顺序调用析构函数。

      如果不这样做,您就会出现未定义的行为。通常UB是基类dtor被调用。

      【讨论】:

        【解决方案4】:

        如果函数不是virtual,C++运行时会直接调用ma​​ngled函数。例如,在上面的代码中,析构函数可能被修改为_ZNK3AXXXXXXXXX(假名)。所以当你调用delete pt时,运行时会执行_ZNK3AXXXXXXXXX

        但是,如果函数是virtual,结果会有所不同。就像@Yakk 所说,具有虚函数的类将有一个vtable,其条目是函数指针。它可能位于此类地址空间的顶部,也可能位于底部,具体取决于类模型的实现。 virtual 函数的任何调用都会查找此表。如果 dtor 是virtual,则派生类中的函数将覆盖表中的相应条目。当您使用指针或引用调用它时,C++ 运行时将调用表中的函数。再看看你的例子。 pt 指向的对象的条目已被类 B 覆盖,因此当您调用 delete pt 时,将调用 dtor 的覆盖版本。这就是区别。

        【讨论】:

        • “C++ 运行时将调用表中的函数” - 这是一个实现细节,而不是语言规范的一部分。在大多数情况下,是编译器通过 v-table 生成间接调用。无论如何,任何实现都可以选择以任何它喜欢的方式实现虚拟调度。
        • vtable 方法恰好简单、干净、快速,只依赖于本地信息(无需进行全局分析),它是 O(1)... 它有很多不错的属性,实际上,每个不是解释器的实现都会使用它。
        猜你喜欢
        • 1970-01-01
        • 2011-08-12
        • 1970-01-01
        • 2012-04-18
        • 2020-04-29
        • 2019-06-26
        • 2011-12-06
        • 2010-10-24
        • 2017-05-31
        相关资源
        最近更新 更多