题 何时使用虚拟析构函数?


我对大多数面向对象的理论有深刻的理解,但令我困惑的一件事是虚拟析构函数。

我认为无论什么和链中的每个对象,析构函数总是被调用。

你什么时候打算让它们成为虚拟的?为什么?


1208
2018-01-20 12:58


起源


看到这个: 虚拟析构函数 - Naveen
每个析构函数 下 无论如何都被召唤。 virtual 确保它从顶部而不是中间开始。 - Mooing Duck
相关问题: 什么时候不应该使用虚拟析构函数? - Eitan T
很多很多年前,我是一名C ++程序员,这是最常见的面试问题。 - Ian Ringrose
@FranklinYu你问的很好,因为现在我看不出该评论的任何问题(除了试图在评论中给出答案)。 - Euri Pinhollow


答案:


当您可以通过指向基类的指针删除派生类的实例时,虚拟析构函数很有用:

class Base 
{
    // some virtual methods
};

class Derived : public Base
{
    ~Derived()
    {
        // Do some important cleanup
    }
};

在这里,您会注意到我没有声明Base的析构函数 virtual。现在,我们来看看以下代码段:

Base *b = new Derived();
// use b
delete b; // Here's the problem!

因为Base的析构函数不是 virtual 和 b 是一个 Base* 指着一个 Derived 目的, delete b 具有 未定义的行为

[在 delete b],如果是静态类型的   要删除的对象不同于其动态类型,静态   type应该是对象的动态类型的基类   删除和 静态类型应具有虚拟析构函数或   行为未定义

在大多数实现中,对析构函数的调用将像任何非虚拟代码一样被解析,这意味着将调用基类的析构函数而不是派生类的析构函数,从而导致资源泄漏。

总而言之,总是要创建基类的析构函数 virtual 当他们打算被多态地操纵时。

如果要防止通过基类指针删除实例,可以使基类析构函数受保护且非虚拟化;通过这样做,编译器不会让你调用 delete 在基类指针上。

您可以在中了解有关虚拟性和虚拟基类析构函数的更多信息 来自Herb Sutter的这篇文章


1315
2018-01-20 13:04



这可以解释为什么我使用我之前制造的工厂发生了大量泄漏。一切都有意义。谢谢 - Lodle
如果指针是无效*,这也可以工作吗? - Lodle
不,它不会。无效指针不知道析构函数。 - Leon Timmermans
不,它不适用于void *。编译器对void *指向的内容一无所知。它只知道它是一个内存位置。您需要将指针强制转换为类型,以告诉编译器有什么。 - Rob K
来自Herb Sutter的文章:“准则#4:基类析构函数应该是公共的和虚拟的,或者是受保护的和非虚拟的。” - Sundae


在多态基类中声明析构函数是虚拟的。这是Scott Meyers的第7项 有效的C ++。迈耶斯接着总结说,如果一个班级有 任何 虚函数,它应该有一个虚拟析构函数,那些不是基类或不设计为多态使用的类应该  声明虚拟析构函数。


165
2018-01-20 13:11



+“如果一个类有任何虚函数,它应该有一个虚析构函数,那些不是基类或不设计为多态的类不应该声明虚拟析构函数。”:是否存在有意义的情况打破这个规则?如果没有,让编译器检查这个条件是否有意义并发出错误是否不满意? - Giorgio
@Giorgio我不知道该规则有任何例外。但我不认为自己是C ++专家,所以你可能想把它作为一个单独的问题发布。编译器警告(或来自静态分析工具的警告)对我有意义。 - Bill the Lizard
类可以设计为不通过某个类型的指针删除,但仍然具有虚函数 - 典型的例子是回调接口。一个人不会通过回调接口指针删除他的实现,因为它只用于订阅,但它确实有虚函数。 - dascandy
@dascandy完全 - 那个或全部 许多 我们使用多态行为但不通过指针执行存储管理的其他情况 - 例如维护自动或静态持续时间对象,指针仅用作观察路径。在任何此类情况下都不需要/目的来实现虚拟析构函数。由于我们只是引用了这里的人,我更喜欢上面的Sutter:“准则#4:基类析构函数应该是公共的和虚拟的,或者是受保护的和非虚拟的。”后者确保任何人意外地尝试通过基指针删除都会显示出他们的方式错误 - underscore_d
@Giorgio实际上有一个可以使用的技巧并避免对析构函数进行虚拟调用:通过const引用将派生对象绑定到基类,如 const Base& = make_Derived();。在这种情况下,析构函数 Derived prvalue将被调用,即使它不是虚拟的,因此可以节省vtable / vpointers引入的开销。当然,范围非常有限。 Andrei Alexandrescu在他的书中提到了这一点 现代C ++设计。 - vsoftco


虚拟构造函数是不可能的,但虚拟析构函数是可能的。 让我们实验吧......

#include <iostream>

using namespace std;

class Base
{
public:
    Base(){
        cout << "Base Constructor Called\n";
    }
    ~Base(){
        cout << "Base Destructor called\n";
    }
};

class Derived1: public Base
{
public:
    Derived1(){
        cout << "Derived constructor called\n";
    }
    ~Derived1(){
        cout << "Derived destructor called\n";
    }
};

int main()
{
    Base *b = new Derived1();
    delete b;
}

上面的代码输出如下:

Base Constructor Called
Derived constructor called
Base Destructor called

派生对象的构造遵循构造规则,但是当我们删除“b”指针(基指针)时,我们发现只有基本析构函数被调用。但是这不能发生。要做适当的事情,我们必须使基础析构函数成为虚拟的。 现在让我们看看下面发生了什么:

#include <iostream>

using namespace std;

class Base
{ 
public:
    Base(){
        cout << "Base Constructor Called\n";
    }
    virtual ~Base(){
        cout << "Base Destructor called\n";
    }
};

class Derived1: public Base
{
public:
    Derived1(){
        cout << "Derived constructor called\n";
    }
    ~Derived1(){
        cout << "Derived destructor called\n";
    }
};

int main()
{
    Base *b = new Derived1();
    delete b;
}

输出更改如下:

Base Constructor Called
Derived constructor called
Derived destructor called
Base Destructor called

因此,基指针的破坏(在派生对象上进行分配!)遵循破坏规则,即首先导出然后是基数。 另一方面,对于构造函数,没有像虚构造函数那样的东西。


155
2018-04-09 13:39



“虚拟构造函数不可能”意味着您不需要自己编写虚拟构造函数。派生对象的构造必须遵循从派生到基础的构造链。因此,您无需为构造函数编写virtual关键字。谢谢 - Tunvir Rahman Tusher
@Murkantilism,“虚拟构造函数无法完成”确实如此。构造函数不能标记为虚拟。 - cmeub
@cmeub,但是有一个习惯用来实现你想要的虚拟构造函数。看到 parashift.com/c++-faq-lite/virtual-ctors.html - cape1232
@TunvirRahmanTusher你能解释一下为什么要调用Base Destructor吗? - rimiro
@rimiro它是c ++自动的。你可以点击链接 stackoverflow.com/questions/677620/... - Tunvir Rahman Tusher


还要注意,当没有虚拟析构函数时,将导致删除基类指针 未定义的行为。我最近刚学到的东西:

如何在C ++中覆盖删除行为?

我已经使用C ++多年了,我仍然设法自己挂起。


37
2018-01-21 01:09



我看了你的那个问题,发现你已经将基础析构函数声明为虚拟。那么“在没有虚拟析构函数的情况下删除基类指针会导致未定义的行为”对于你的问题保持有效吗?因为,在该问题中,当您调用delete时,首先检查派生类(由其新运算符创建)的兼容版本。因为它在那里找到了它,它被称为。那么,你不认为最好说“当没有析构函数时会删除基类指针会导致未定义的行为”吗? - ubuntugod
这几乎是一回事。默认构造函数不是虚拟的。 - BigSandwich


当您的类具有多态性时,使析构函数成为虚拟的。


30
2018-01-20 13:01





通过指向基类的指针调用析构函数

struct Base {
  virtual void f() {}
  virtual ~Base() {}
};

struct Derived : Base {
  void f() override {}
  ~Derived() override {}
};

Base* base = new Derived;
base->f(); // calls Derived::f
base->~Base(); // calls Derived::~Derived

虚拟析构函数调用与任何其他虚函数调用没有区别。

对于 base->f(),电话将被发送到 Derived::f(),它也是一样的 base->~Base()  - 它最重要的功能 - Derived::~Derived() 将被召唤。

当间接调用析构函数时也会发生相同的情况,例如 delete base;。该 delete 声明将致电 base->~Base() 将被派遣到 Derived::~Derived()

具有非虚拟析构函数的抽象类

如果您不打算通过指向其基类的指针删除对象 - 那么就不需要有虚拟析构函数。做吧 protected 这样它就不会被意外调用:

// library.hpp

struct Base {
  virtual void f() = 0;

protected:
  ~Base() = default;
};

void CallsF(Base& base);
// CallsF is not going to own "base" (i.e. call "delete &base;").
// It will only call Base::f() so it doesn't need to access Base::~Base.

//-------------------
// application.cpp

struct Derived : Base {
  void f() override { ... }
};

int main() {
  Derived derived;
  CallsF(derived);
  // No need for virtual destructor here as well.
}

10
2018-05-18 13:38



是否有必要明确声明 ~Derived() 在所有派生类中,即使它只是 ~Derived() = default?或者是语言所隐含的(可以省略)? - Ponkadoodle
@Wallacoloo不,只在必要时才声明。例如。摆在 protected部分,或通过使用确保它是虚拟的 override。 - Abyx


我喜欢考虑接口的接口和实现。在C ++中,speak接口是纯虚拟类。析构函数是界面的一部分,有望实现。因此析构函数应该是纯虚拟的。构造函数怎么样?构造函数实际上不是接口的一部分,因为对象始终是显式实例化的。


7
2017-11-08 16:28



这如何改善已经接受的答案? - cale_b
这是对同一个问题的不同看法。如果我们根据接口而不是基类与派生类进行思考,那么它就是自然的结论:如果它是接口的一部分而不是虚拟化。如果不是这样的话。 - Dragan Ostojic
+1表示OO概念的相似性 接口 和一个C ++ 纯虚拟课程。关于 析构函数有望实现:这通常是不必要的。除非类正在管理诸如原始动态分配的内存之类的资源(例如,不通过智能指针),文件句柄或数据库句柄,否则使用编译器创建的默认析构函数在派生类中是很好的。请注意,如果声明了析构函数(或任何函数) virtual 在基类中,它是自动的 virtual 在派生类中,即使它没有声明。 - DavidRR
这错过了析构函数的关键细节 不必要 界面的一部分。可以轻松编写具有多态函数但调用者不管理/不允许删除的类。然后虚拟析构函数没有任何意义。当然,为了确保这一点,非虚拟 - 可能是默认的 - 析构函数应该是非公开的。如果我不得不猜测,我会说这些类经常在内部用于项目,但这并不会使它们作为一个例子/细微差别在所有这些中不那么相关。 - underscore_d