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;
} |
输出为:
我的问题是:
基类的析构函数不能被派生类继承,那么为什么我们要使基类的析构函数为虚拟的呢?
对于上面的代码,我知道这将导致问题(这里不调用类B的析构函数)。我从Google和StackOverflow中搜索了这么多的文章或问题,它们都告诉我,基本的析构函数应该是虚拟的,但析构函数上的"虚拟"是如何工作的?我的意思是,对于析构函数来说,有/没有"虚拟"的核心代码级别有什么区别?
- "基类的析构函数不能被派生类继承"-顺便说一下,你为什么这么认为?
- 如果派生类析构函数是虚拟的,那么对象将按顺序(先是派生对象,然后是基对象)进行分解。如果派生类析构函数不是虚拟的,那么将只删除基类对象
- 你熟悉vtables吗?stackoverflow.com/a/99341/951890
- @Hariom Singh,你说的是告诉我们现象,但不能解释原因。这就是我发表问题的原因。
- 在这种情况下,析构函数中的virtual与virtual函数没有任何不同:它只是确保调用实际对象实例的析构函数。什么让你困惑?
- @iInspectable,请参阅本文
- @AdrianShum,让我困惑的是我认为析构函数不同于成员函数。因为如果~a()是虚拟的,我们就不能重写类B中的~a()。
- @马申静,我明白它是如何迷惑你的。从概念上讲,您可能只是将~ClassName()作为定义"析构函数方法"的特殊名称。C++可以选择"DeExtruter方法"的名称为EDCOX1(3)}(您不应该混淆为什么EDCOX1(4)在这里起作用),尽管他们已经选择将其命名为EDCOX1(2)。
- 对引用的文本使用引号格式,而对未引用的文本不使用引号格式。引用时请引用源代码。
- 析构函数在某种意义上可以被视为特殊的,派生类中的构造函数不"重写"基类中的构造函数,而是"链接"到它们(虚拟的或非虚拟的)。
如果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。这就是区别。
- "C++运行时将调用表中的函数"——这是实现细节,而不是语言规范的一部分。在大多数情况下,编译器通过v-table生成间接调用。无论如何,任何实现都可以选择以它喜欢的任何方式实现虚拟调度。
- vtable方法非常简单、干净和快速,只依赖于本地信息(不需要进行全局分析),它是o(1)……实际上,它有许多好的属性,每个不是解释器的实现都将使用它。