关于未定义的行为:“虚拟”如何影响C ++中的析构函数?

How “virtual” impact on destructor in C++?

官方解释的虚拟功能是:

A virtual function is a member function that you expect to be redefined in derived classes. When you refer to a derived class object using a pointer or a reference to the base class, you can call a virtual function for that object and execute the derived class's version of the function.

请先看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#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;
}

输出为:

1
2
3
A()
B()
~A()

我的问题是:

  • 基类的析构函数不能被派生类继承,那么为什么我们要使基类的析构函数为虚拟的呢?
  • 对于上面的代码,我知道这将导致问题(这里不调用类B的析构函数)。我从Google和StackOverflow中搜索了这么多的文章或问题,它们都告诉我,基本的析构函数应该是虚拟的,但析构函数上的"虚拟"是如何工作的?我的意思是,对于析构函数来说,有/没有"虚拟"的核心代码级别有什么区别?

  • 如果A的析构函数不是虚拟的,delete pt;会导致不定义的行为。

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

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

    其他一些注释/答案表明,原始代码的定义行为是不调用B的析构函数或其他东西。但那是错误的。你只是看到了行为不明确的症状,可能是那样或其他什么。


    它可以帮助您想象vtables是如何实现的。

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

    方法上的virtual意味着它的virtual function表中有一个条目。

    对于方法,继承的类在重写时替换条目。

    对于析构函数,条目实际上是"如何调用此对象上的delete"。所有派生类都会自动覆盖它。它从概念上把delete base_ptr的调用转化为if (base_ptr) base_ptr->vtable->deleter(base_ptr);

    那么,派生的删除程序实际上(几乎)是delete static_cast(ptr);,它执行通常的删除调用所执行的操作,它按顺序调用析构函数。

    如果不这样做,你的行为就不明确了。通常,ub是调用基类dtor。


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

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


    如果函数不是EDCOX1(12),则C++运行时将直接调用被破坏的函数。例如,在上述代码中,析构函数可能被破坏为_ZNK3AXXXXXXXXX(假名)。所以当您调用delete pt时,运行时将执行_ZNK3AXXXXXXXXX

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