何时使用虚析构函数?

1876

我对大部分的对象导向编程(OOP)理论有扎实的了解,但有一件事情经常让我困惑,那就是虚析构函数。

我曾以为无论如何,析构函数都会被调用,并且每个对象都要调用一次。

什么时候需要将析构函数声明为虚函数?为什么需要这样做呢?


8
请看:虚析构函数这篇文章讨论了在使用C++的时候,是否每个类都应该有一个虚析构函数。如果类被设计成应该被继承,那么最好给它一个虚析构函数,以避免可能的内存泄漏问题。但是,如果类不会被继承或者是一个简单的数据容器,那么就没有必要给它一个虚析构函数。 - Naveen
204
无论如何,每个析构函数 _down_ 都会被调用。 virtual 确保它从顶部开始而不是中间开始。 - Mooing Duck
19
相关问题:何时不应使用虚析构函数?当一个类没有任何虚函数时,通常不需要为其定义虚析构函数。但如果该类会被继承,并且在派生类中使用了动态内存分配,则必须为基类定义虚析构函数,以确保正确释放内存。否则,当删除指向派生类对象的基类指针时,可能会导致未定义的行为。 - Eitan T
5
我也对@MooingDuck的回答感到困惑。如果使用子类(下方)和超类(上方)的概念,它不应该是“up”而不是“down”吗? - Nibor
4
@Nibor:是的,_如果你使用那个概念的话_。我与之交谈的人中有一半认为超类是“上方”的,另一半则认为超类是“下方”的,因此两种标准存在冲突,这使得一切都很混乱。我认为将超类视为“上方”略微更常见,但我并没有被教导成这样。 - Mooing Duck
显示剩余5条评论
20个回答

1906

虚析构函数在通过指向基类的指针删除派生类实例时非常有用:

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是指向Derived对象的Base*,所以delete b存在未定义行为

delete b中,如果要删除的对象的静态类型与其动态类型不同,则静态类型应为要删除的对象的动态类型的基类,并且静态类型必须具有虚析构函数,否则行为未定义

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

总之,当希望通过多态方式操作时,务必使基类的析构函数为 virtual

如果想要防止通过基类指针删除实例,可以将基类的析构函数设为受保护的和非虚拟的。 这样做,编译器将不允许您在基类指针上调用delete

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


255
这可以解释为什么我之前用自己制作的工厂会出现大量泄漏。现在一切都有意义了。谢谢。 - Lodle
16
这个例子不太好,因为没有数据成员。如果BaseDerived都只有自动存储变量,也就是说,在析构函数中没有“特殊”的或额外的自定义代码要执行,那么可以不编写任何析构函数吗?还是派生类仍然会有内存泄漏? - bobobobo
4
等一下,这将导致未定义行为(Undefined Behavior)。 - bobobobo
51
来自Herb Sutter的文章中的指南#4:基类的析构函数应该是public和virtual,或者是protected和nonvirtual。 - Sundae
6
文章中还提到:“如果你在没有虚析构函数的情况下进行多态删除,你会召唤出可怕的“未定义行为”的幽灵。说实话,即使在照明程度相对较好的小巷里,我也不想遇见它。”哈哈 - Bondolin
显示剩余11条评论

316

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

#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

因此,基指针的销毁(需要在派生对象上进行分配!)遵循销毁规则,即先销毁Derived,然后再销毁Base。 另一方面,并不存在所谓的虚构造函数。


1
"虚拟构造函数不可行" 意味着您无需自己编写虚拟构造函数。派生对象的构建必须遵循从派生到基类的构建链。因此,您无需为构造函数编写虚拟关键字。谢谢。 - Tunvir Rahman Tusher
5
@Murkantilism所说的“虚拟构造函数不可行”确实是正确的。构造函数不能被标记为虚拟。 - cmeub
1
@cmeub,但是有一种习惯用法可以实现您从虚拟构造函数中想要的效果。请参见http://www.parashift.com/c++-faq-lite/virtual-ctors.html。 - cape1232
@TunvirRahmanTusher,您能否解释一下为什么会调用基本析构函数? - rooni
@rimiro 这是C++自动完成的,你可以按照以下链接操作 https://dev59.com/OXRB5IYBdhLWcg3wQVSV - Tunvir Rahman Tusher
第一个例子在基类中没有虚析构函数,这不是未定义行为吗? - xyf

222

在多态的基类中声明析构函数为虚函数。这是Scott Meyers在他的《Effective C++》中的第7条建议。Meyers继续总结道:如果一个类有任何虚函数,则它应该有一个虚析构函数,并且不是设计为基类或者不是设计为多态使用的类不应该声明虚析构函数。


20
如果一个类有任何虚函数,它应该有一个虚析构函数,而那些不设计为基类或不设计为多态使用的类不应声明虚析构函数。是否存在违反此规则有意义的情况?如果不存在这样的情况,是否有意义让编译器检查此条件并在不满足时发出错误信息? - Giorgio
12
可以设计类,使其不能通过某种类型的指针进行删除,但仍具有虚函数。典型的例子是回调接口。通过回调接口指针不会删除其实现,因为它只用于订阅,但它确实具有虚函数。 - dascandy
5
@dascandy 没错 - 或者在许多其他情况下使用多态行为但不通过指针执行存储管理 - 例如,维护自动或静态持续时间对象,仅使用指针作为观察路线。在任何这样的情况下都没有必要/目的实现虚析构函数。由于我们只是引用别人的话,我更喜欢来自上面的 Sutter 的指导方针:“准则#4:基类析构函数应该是公共且虚拟的,或者受保护且非虚拟的。” 后者确保任何意外尝试通过基指针删除的人都会看到他们错误的地方。 - underscore_d
1
@Giorgio 实际上有一个技巧可以避免虚函数调用析构函数:通过将派生对象绑定到基类的const引用上,例如 const Base& = make_Derived();。在这种情况下,即使它不是虚拟的,也会调用Derived prvalue的析构函数,因此可以节省vtables/vpointers引入的开销。当然,范围相当有限。Andrei Alexandrescu在他的书现代C++设计中提到了这一点。 - vsoftco
如果没有多态析构函数,为什么没有虚拟方法的事情会起作用?编辑回答:事实上是不行的,但在这种情况下,您永远不需要编写 Base * b = new Derived1,这是所有问题的源头,而应该使用Derived1 * b = new Derived1。此外,如果没有new,就不会有问题,因为编译器总是知道任何超出作用域的实际类型。 - Ciro Santilli OurBigBook.com
显示剩余2条评论

49

如果你的类是多态的,那么请将析构函数声明为虚函数。


49

请注意,如果没有虚析构函数,则删除基类指针将导致未定义的行为。这是我最近才学到的:

如何在C++中覆盖删除(delete)操作?

我已经使用C++多年了,但仍然会自己给自己挖坑。


我看了一下你的那个问题,并发现你已经将基础析构函数声明为虚函数。因此,关于你的那个问题,“当没有虚析构函数时删除基类指针会导致未定义行为”的说法是否仍然有效呢?因为在那个问题中,当你调用delete时,首先检查派生类(由其new运算符创建)是否有兼容版本。既然找到了一个兼容版本,就会被调用。所以,难道不应该说“当没有析构函数时删除基类指针会导致未定义行为”更好吗? - ubuntugod
这几乎是同样的事情。默认构造函数不是虚拟的。 - BigSandwich
@BigSandwich "吊死自己"?你是指内存泄漏吗? - John

18

Calling destructor via a pointer to a base class

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.
}

即使只是 ~Derived() = default,在所有派生类中都需要显式声明 ~Derived() 吗?或者这是语言所暗示的(因此可以安全地省略)? - Ponkadoodle
@Wallacoloo 不需要一直声明,只有在必要的时候才需要。例如将其放入“protected”部分或使用“override”确保其为虚函数。 - Abyx
@Abyx 调用 base->~Base() 是否合适?根据您所说,Base::~Base() 不会被调用,那么就会出现内存泄漏。我是对的吗? - John

15

当您希望通过基类指针删除对象时,不同的析构函数应该按正确顺序进行时,虚拟析构关键字是必需的。

例如:

Base *myObj = new Derived();
// Some code which is using myObj object
myObj->fun();
//Now delete the object
delete myObj ; 
如果你的基类析构函数是虚函数,那么对象将按顺序被销毁(首先是派生对象,然后是基类)。如果你的基类析构函数不是虚函数,那么只有基类对象会被删除(因为指针是基类 "Base *myObj")。因此,派生对象会造成内存泄漏。

13
为简单起见,虚析构函数是在删除指向派生类对象的基类指针时按适当顺序销毁资源的。
 #include<iostream>
 using namespace std;
 class B{
    public:
       B(){
          cout<<"B()\n";
       }
       virtual ~B(){ 
          cout<<"~B()\n";
       }
 };
 class D: public B{
    public:
       D(){
          cout<<"D()\n";
       }
       ~D(){
          cout<<"~D()\n";
       }
 };
 int main(){
    B *b = new D();
    delete b;
    return 0;
 }

OUTPUT:
B()
D()
~D()
~B()

==============
If you don't give ~B()  as virtual. then output would be 
B()
D()
~B()
where destruction of ~D() is not done which leads to leak


没有基类虚析构函数并且在基类指针上调用 delete 会导致未定义的行为。 - James Adkison
@JamesAdkison 为什么会导致未定义行为?? - rooni
@rimiro 这就是标准规定的内容。我没有副本,但链接会带您到一个评论中,其中有人引用了标准内的位置。 - James Adkison
@rimiro “如果删除可以通过基类接口进行多态操作,那么它必须是虚拟的并且必须是虚拟的。实际上,语言要求这样做-如果您在没有虚拟析构函数的情况下进行多态删除,则会召唤“未定义行为”的可怕幽灵,我个人甚至不想在一个相当明亮的小巷里遇到它,非常感谢。”(http://www.gotw.ca/publications/mill18.htm)-- Herb Sutter - James Adkison

10

我喜欢思考接口和接口实现。在C++中,接口是纯虚类。析构函数是接口的一部分,需要被实现。因此,析构函数应该是纯虚的。那么构造函数呢?构造函数实际上不是接口的一部分,因为对象总是显式地实例化。


2
这是同一个问题的不同视角。如果我们从接口而不是基类 vs 派生类的角度来思考,那么自然地得出结论:如果它是接口的一部分,则将其设置为虚方法。否则就不需要。 - Dragan Ostojic
2
+1 表示 OO 概念中的 接口 和 C++ 中的 纯虚类 相似。关于 预期实现析构函数:这通常是不必要的。除非一个类正在管理资源,例如原始动态分配的内存(例如,不通过智能指针)、文件句柄或数据库句柄,在派生类中使用编译器创建的默认析构函数就可以了。请注意,如果在基类中声明了析构函数(或任何函数)为 virtual,则即使在派生类中没有声明为 virtual,它也会自动成为 virtual - DavidRR
这里缺少一个关键细节,即析构函数不一定是接口的一部分。人们可以轻松编写具有多态函数但调用方不管理/不允许删除的类。那么虚析构函数就没有作用了。当然,为了确保这一点,非虚拟(可能是默认的)析构函数应该是非公开的。如果我必须猜测,我会说这样的类更常用于项目内部,但这并不使它们在所有这些例子/细微差别中变得不那么重要。 - underscore_d

5
我建议这样做:如果一个类或结构体不是final,则应为其定义虚析构函数。 我知道这看起来像是过度保守的规则。但是,这是唯一确保从您的类派生的人在使用基指针删除时不会有UB的方法。
Scott Meyers在Effective C ++中的推荐如下所述是好的,但不能确保:

如果一个类有任何虚函数,则应该有一个虚析构函数,并且不设计为基类或不设计为多态使用的类不应声明虚析构函数。

例如,在下面的程序中,基类B没有任何虚函数,因此根据Meyer,您不需要编写虚析构函数。 但是,如果不这样做,您将具有UB:
#include <iostream>

struct A
{
    ~A()
    {
        std::cout << "A::~A()" << std::endl;
    }
};

struct B
{
};

struct C : public B
{
    A a;
};

int main(int argc, char *argv[])
{
    B *b = new C;
    delete b; // UB, and won't print "A::~A()"
    return 0;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接