"virtual"对C++析构函数有何影响?

How "virtual" impact on destructor in C++?

本文关键字:影响 virtual C++ 析构函数 何影响      更新时间:2023-10-16

官方解释中的虚函数是:

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


请先看代码:

#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 类的析构函数(。我从谷歌和堆栈溢出搜索了很多文章或问题,他们都告诉我 base 的析构函数应该是虚拟的,但析构函数上的"虚拟"如何工作?我的意思是,有/没有"虚拟"的核心代码级别与析构函数有什么区别?

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

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

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

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

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

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

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

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

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

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

对于析构函数,条目实际上是"如何在此对象上调用 delete"。 所有下属类都会自动覆盖它。 它将delete base_ptr的呼唤在概念上变成了if (base_ptr) base_ptr->vtable->deleter(base_ptr);

然后派生的删除器实际上是(几乎(delete static_cast<derived*>(ptr);这执行通常的删除调用所做的事情,它按顺序调用析构函数。

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

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

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