你的析构函数应该何时使用虚函数?

46

2
从 http://blogs.msdn.com/oldnewthing/archive/2004/05/07/127826.aspx 复制? - Stobor
很多相关的内容:http://stackoverflow.com/search?q=virtual+destructor - aJ.
尝试使用此链接https://dev59.com/IHRB5IYBdhLWcg3w77on#15903538。它可能会有所帮助。 - Tunvir Rahman Tusher
6个回答

61
  1. 当类中至少有一个方法是虚函数时,你需要使用虚析构函数。

因为使用虚函数的原因是使用多态。这意味着你将在基类指针上调用一个方法,并且你想要最终实现 - 这就是多态的整个意义。

现在,如果你没有虚析构函数,而是通过基类指针调用析构函数,那么你最终会调用基类的析构函数。在这种情况下,你希望多态性也能在析构函数上工作,例如通过在基类上调用析构函数,你希望调用最终派生类的析构函数,而不是基类的析构函数。

class A
{
   virtual void f() {}
   ~A() {}
}

class B : public A
{
   void f() {}
   ~B() {}
}

A * thing = new B();
thing->f(); // calls B's f()
delete thing; // calls ~A(), not what you wanted, you wanted ~B()

将 ~A() 声明为虚函数可以启用多态性

virtual ~A() {}

所以当你现在调用时

delete thing;

~B()将会被调用。

当你设计一个类作为接口时,你应该声明虚析构函数,例如你希望它被扩展或实现。在这种情况下,一个好的做法是拥有一个接口类(类似于Java中的接口)具有虚方法和虚析构函数,然后有具体实现类。

可以看到STL类没有虚析构函数,因此不应该被扩展(例如std::vector、std::string...)。如果你扩展了std::vector并且通过指针或引用调用基类的析构函数,你肯定不会调用你专门的类析构函数,这可能会导致内存泄漏。


Pluralsight技能测评问题中选择错误答案后,您提供了我寻找的确切答案,非常感谢。 - sebkraemer

33

来自 Stroustrup的C++风格和技术FAQ

那么什么时候应该声明虚析构函数?每当类至少具有一个虚函数时。拥有虚函数意味着该类旨在作为接口对派生类进行操作,当这样做时,派生类的对象可以通过指向基类的指针被销毁。

关于何时应将析构函数声明为虚拟的,有很多额外信息在C++ FAQ上。(感谢Stobor)

什么是虚拟成员?来自C++ FAQ

[20.1]什么是“虚成员函数”?

从面向对象的角度来看,它是C++中最重要的特性:[6.9],[6.10]。

虚函数允许派生类替换基类提供的实现。编译器确保在对象实际上是派生类时始终调用替换,即使通过基指针而不是派生指针访问对象。这允许在派生类中替换基类中的算法,即使用户不知道派生类也能如此。

派生类可以完全替换(“覆盖”)基类成员函数,或者派生类可以部分替换(“扩充”)基类成员函数。后一种情况是通过让派生类成员函数调用基类成员函数来实现的,如果需要的话。


那不是唯一的情况,然而... - Stobor
+1,Mr S. 的引用非常好,真的无法超越这个答案。 - stefanB
2
准确来说,如果没有虚函数的父类的子类定义了一个需要清理但不包含在父类中的成员,那么怎么办呢?缺少虚析构函数意味着“delete parent”不会调用子类的析构函数... - Stobor
1
http://www.parashift.com/c++-faq-lite/virtual-functions.html#faq-20.7 - Stobor
2
在这种情况下,Stobor,实例化派生类几乎没有意义。没有办法访问派生类的方法,除非进行dynamic_cast,这意味着需要了解对象类型。这种知识也可以在析构函数之前使用dynamic_cast。当然,虚方法只是一个经验法则。毫不奇怪,你编造的例子打破了这个规则。 - Tom Leys
1
在极为罕见的情况下,如果 Stroustrup 的推断不成立(即类旨在充当派生类的接口,但所需接口是不允许通过基类指针销毁派生类对象),则我认为您可以使用受保护的非虚拟析构函数。据我所知,将其设置为非虚拟几乎没有意义,因为虚拟析构函数几乎不会成为常见的性能瓶颈。但是,阻止客户端自己删除内容可能是一个有用的限制,如果需要,它可以是非虚拟的。 - Steve Jessop

6
我最近得出的结论是完全正确的答案是这样的:
指南 #4:基类析构函数应该是公共的和虚拟的,或者是受保护的和非虚拟的。
当然,Herb Sutter 给出了他的理由。请注意,他超越了通常的答案:“当某人通过基类指针删除派生类对象”和“如果您的类有任何虚拟函数,请使您的析构函数虚拟”。

1
我不会将其缩小到这两个选项。您使用工具构建所需的内容,例如编程语言的功能。如果您将每个公共析构函数都设置为公共,则为这些类中的每个类打开多态性,在90%的情况下您不需要它,并且最终会产生不必要的开销。 - stefanB

3
如果你会(或者可能会)通过基类指针销毁派生类的对象,那么你需要一个虚析构函数。我的做法是,如果我从一个类派生出来,那么它就应该有一个虚析构函数。在我编写的代码中,实际上没有任何情况需要考虑虚析构函数的性能影响,即使今天不需要,将来修改类时也可能需要。基本上:除非你有充分考虑的理由,否则所有基类的析构函数都应该加上virtual。这只是另一条经验法则,但它可以避免以后的错误。

0

始终如此。

除非我真的关心vtable的存储和性能开销,我总是将其设为虚拟的。除非你有一个静态分析工具可以验证你的析构函数在正确的情况下是虚拟的,否则在需要时没有做出虚拟析构函数而犯错是不值得的。


1
C++并不是为了让你抛弃它的灵活性而存在的。换句话说,“除非我真的关心vtable的存储和性能开销,否则我会使用像Python或Lua这样更容易的语言。” - Tom
1
"C语言让你容易自己踢到自己的脚;C++则更难,但一旦出错,就会把整条腿都炸掉" --Stroustrup。在正确的情况下,C++是一种非常有用的语言,但你必须保护自己。要么总是将其设为虚拟的,要么找到一个静态分析工具来保护自己,或者在有人更改代码时手动审查每个析构函数。 - Jared Oberhaus
@Jared:或者引入合理的规则和手段来记录哪些类可以用作基类,以及如何使用。您不必为每个代码更改都审查析构函数,只需为更改影响类的多态性特征(无/静态/动态)的更改进行审查。话虽如此,如果您倾向于对所有内容使用动态多态性,则使类继承准备好除非另有证明肯定更容易。前Java程序员可能需要比前C程序员更多的虚拟析构函数和方法,因此我想可以选择“默认虚拟”。 - Steve Jessop
3
@Tom:是的,你可以随时放弃灵活性。C++ 给了你灵活性,这样你就可以在需要时去掉它或者添加它。其他一些语言则一直强制执行它。因此,在 C++ 中,最好在所有地方都加上虚析构函数,除非你已经考虑过并决定不需要它们的那些情况。 - gbjbaanb

-1
一个基类对象应该有一个虚析构函数,当基类需要进行自己的清理时。也就是说,如果你在基类中分配了资源,那么基类必须进行清理。通过声明其析构函数为虚拟函数,您可以保证这个清理将会被执行(假设您正确编写了清理代码)。
通常情况下,可以在基类中定义虚拟方法,这将允许派生类重写虚拟方法,实现自己的特定实现。我认为这最好通过一个简单的例子来说明。假设我们有一个基类“形状”,现在所有派生类都可能需要具有绘制能力。'Shape' 对象不知道如何绘制从它派生的类,因此在 'Shape' 类中我们定义一个虚拟绘制函数。即 (virtual void draw();)。现在在每个基类中,我们可以重写这个函数,实现特定的绘制代码(例如,正方形和圆形的绘制方式不同)。

1
这并不完全正确,例如,如果一个基类A(没有定义虚析构函数)没有资源,而类B(A的子类)有资源,并且我们有类似于B *b = new B(); A *a = static_cast<A>(b); delete a;的代码,那么结果在标准中实际上是未定义的。它可能释放资源,也可能不释放。往往情况下会造成内存泄漏。所以正确的答案是 - 如果你有子类,那么基类需要有一个虚析构函数来确保正确释放资源。 - hookenz
以下是关于编程的内容,需要从英语翻译成中文,仅返回翻译后的文本:已经包含在指南 C.35 中:https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rc-dtor-virtual - Marine Galantin

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