“虚拟”如何影响C++中的析构函数?

“虚拟”如何影响C++中的析构函数?,c++,undefined-behavior,delete-operator,vtable,virtual-destructor,C++,Undefined Behavior,Delete Operator,Vtable,Virtual Destructor,官方解释中的虚拟功能是: 虚函数是希望在派生类中重新定义的成员函数。使用指针或基类引用引用派生类对象时,可以为该对象调用虚拟函数并执行派生类版本的函数 请先查看代码: #include<iostream> using namespace std; class A { public: A(){cout << "A()" << endl;} ~A(){cout << "~A()" << endl;} }; class B

官方解释中的虚拟功能是:

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

请先查看代码:

#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;
}
我的问题是:

基类的析构函数不能被派生类继承,所以为什么我们要将基类的析构函数设置为虚拟的呢? 对于上面的代码,我知道这将导致未调用类B的析构函数的问题。我从google和stackoverflow搜索了很多文章或问题,它们都告诉我base的析构函数应该是虚拟的,但析构函数的虚拟性如何呢?我的意思是,对于析构函数,在有/没有虚拟的情况下,核心代码级别有什么区别?
如果析构函数被标记为virtual,那么在调用delete时,将调用已分配对象的动态类型的析构函数。在您的示例中,堆上对象的静态类型是A,而动态类型是B


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

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


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

想象一下vtables是如何实现的

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

方法上的虚拟表示在虚拟函数表中有一个条目

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

对于析构函数,条目实际上是如何调用这个对象上的delete。所有子类都会自动覆盖它。它将delete base_ptr调用转换为if base_ptr base_ptr->vtable->deleterbase_ptr;在概念上

然后,导出的deleter是有效的几乎删除静态_castpr;这就像通常的delete调用一样,它按顺序调用析构函数


如果不这样做,你就会有不确定的行为。通常,UB是调用基类dtor的基础。

这有助于想象vtables是如何实现的

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

方法上的虚拟表示在虚拟函数表中有一个条目

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

对于析构函数,条目实际上是如何调用这个对象上的delete。所有子类都会自动覆盖它。它将delete base_ptr调用转换为if base_ptr base_ptr->vtable->deleterbase_ptr;在概念上

然后,导出的deleter是有效的几乎删除静态_castpr;这就像通常的delete调用一样,它按顺序调用析构函数

如果不这样做,你就会有不确定的行为。通常UB是调用基类dtor的对象。

删除pt;如果的析构函数不是虚拟的,则会导致

使A的析构函数为虚拟的原因是为了启用delete pt;删除一个B对象

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

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

删除pt;如果的析构函数不是虚拟的,则会导致

使A的析构函数为虚拟的原因是为了启用delete pt;删除一个B对象

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

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

< P>如果函数不是虚拟的,C++运行时将直接调用被破坏的函数。例如,在上面的代码中,析构函数可能会被损坏为_znk3axxxxxxxx假名称。因此,当您调用delete pt时,运行时将执行_znk3axxxxxx

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

< P>如果函数不是虚函数,C++运行时将直接调用被破坏的函数。例如,在上面的代码中,析构函数可能会被损坏为_znk3axxxxxxxx假名称。因此,当您调用delete pt时,运行时将执行_znk3axxxxxx


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

基类的析构函数不能被派生类继承-顺便说一句,你为什么这么认为?如果你的派生类析构函数是虚拟的,那么对象将按照先派生对象然后是基类的顺序被销毁。如果派生类析构函数不是虚拟的,那么只有基类对象会被删除。您熟悉vtables吗@哈里姆·辛格,你所说的只是告诉我们这个现象,而不是解释为什么。这就是我提出问题的原因。在本文中,virtual-In-destructor与virtual函数没有什么不同:它只是确保调用了实际对象实例的析构函数。什么让你困惑?基类的析构函数不能被派生类继承-顺便说一句,你为什么这么认为?如果你的派生类析构函数是虚拟的,那么对象将按照先派生对象然后是基类的顺序被析构函数。如果派生类析构函数不是虚拟的,那么只有基类对象会被删除。您熟悉vtables吗@哈里姆·辛格,你所说的只是告诉我们这个现象,而不是解释为什么。这就是我提出问题的原因。在本文中,virtual-In-destructor与virtual函数没有什么不同:它只是确保调用了实际对象实例的析构函数。这里的C++运行库将调用表中的函数——这是实现细节,而不是语言规范的一部分。在大多数情况下,编译器通过v表生成间接调用。无论如何,任何实现都可以选择以任何方式实现虚拟调度。vtable方法恰好简单、干净、快速,只依赖于局部信息,无需进行全局分析,它是O1。。。它有很多好的属性,在实践中,每个解释都不是解释程序使用它。C++运行时将调用表中的函数——这是实现细节,而不是语言规范的一部分。在大多数情况下,编译器通过v表生成间接调用。无论如何,任何实现都可以选择以任何方式实现虚拟调度。vtable方法恰好简单、干净、快速,只依赖于局部信息,无需进行全局分析,它是O1。。。它有很多很好的属性,实际上,每个不是解释器的实现都会使用它。
A()
B()
~A()