每个类都应该有虚析构函数吗?

60

Java和C#支持使用finalsealed关键字定义不能作为基类的类。然而,在C++中,没有很好的方法防止一个类被派生出来,这让类的作者面临两难境地:是否每个类都应该有一个虚析构函数?


编辑:自C++11起,这不再是真实的,您可以指定一个类为final


一方面,给一个对象添加虚析构函数意味着它将拥有一个vtable,因此每个对象会额外消耗4个(在64位机器上为8个)字节用于vptr。
另一方面,如果稍后有人从这个类派生并通过指向基类的指针删除派生类,则程序将是未定义的(由于缺少虚析构函数),而且为了每个对象优化指针是荒谬的。
然而,重要的是拥有虚析构函数(可以说)表明该类型旨在以多态方式使用。
有些人认为你需要明确的理由才能不使用虚析构函数(正如这个问题的潜台词所示),而其他人则说只有在有理由相信你的类将被派生时才应该使用它们,你认为呢?

1
已经有问题询问了利弊 - 这是一个重复的问题,还是意在进行一项调查?如果是后者,也许您应该创建“是”和“否”的答案进行投票,然后关闭问题?我认为这是在 SO 上实现多项选择调查的推荐方式。 - Steve Jessop
1
重复问题:https://dev59.com/Y3VC5IYBdhLWcg3whBaj,https://dev59.com/mHVC5IYBdhLWcg3wbQbA - Steve Jessop
7
“坦率地说,为每个对象优化一个指针是荒谬的。” - 对于小对象来说并不荒谬。C++0x正在添加一个容器forward_list,正是因为有时每个对象的一个指针的开销太大了 - 从空间和时间需求考虑。 - Greg Rogers
1
@onebyone,这个问题与你列出的第一个问题不是重复的,那个问题是针对抽象类的,而我在我的问题中引用了第二个问题,我认为这不是一个重复的问题,因为该问题强烈偏向于有虚拟析构函数,而我想要一个开放的讨论。 - Motti
这回答解决了你的问题吗?为什么在C++中声明抽象类的虚析构函数?(https://dev59.com/Y3VC5IYBdhLWcg3whBaj) - bobobobo
显示剩余2条评论
7个回答

63

每个抽象类应该具有:

  • 受保护的析构函数,或
  • 虚拟析构函数。

如果您有一个公共的非虚拟析构函数,那就不太好了,因为它允许用户通过该指针删除派生对象。正如我们所知道的那样,这是未定义行为。

对于抽象类,您已经需要在对象中使用虚表指针,因此使析构函数 virtual 在空间或运行时性能方面没有太高的成本(据我所知)。并且它的好处是派生类自动拥有其析构函数 virtual(请参见@Aconcagua的评论)。当然,您也可以将析构函数设置为 protected virtual 来处理此情况。

对于非抽象类且不打算通过指向其的指针进行删除的情况,我认为没有充分的理由具有虚析构函数。它会浪费资源,但更重要的是它会给用户错误的提示。只需考虑一下将 std::iterator 设为虚构函数的奇怪感觉即可。


3
或者是从<ctime>中的struct tm,这将不再是POD,因此不再与C调用约定兼容。 - Steve Jessop
好答案,litb。我现在要去改变我的所有代码。 - e.James
晚来一步,我知道 - 但我不同意受保护的析构函数。有人可能会忽视将派生类的公共析构函数再次设置为虚拟的必要性,而其他人可能会从已经假定为虚拟析构函数的派生类派生 - 并(非法地)通过指向子代的指针删除孙代... 当然,第二个人犯了错误,但如果在第一个基类中析构函数已经是虚拟的,就可以避免这种情况,因此从安全角度考虑,我只认为第二个选项有效(当然只适用于抽象类)。 - Aconcagua
@Aconcagua 我对于第二个作者的规则是:每个非抽象可实例化的类应该有一个虚拟公共析构函数,或者非虚拟公共析构函数并声明为 final - Johannes Schaub - litb
你提出为每个抽象类添加虚析构函数的想法听起来很有趣。也许我应该采纳它。我的关于std::iterator的例子有些不太恰当,因为std::iterator根本不是抽象的。我会进行修改。 - Johannes Schaub - litb
@JohannesSchaub-litb >“每个非抽象可实例化类应该有一个虚拟公共析构函数,或者非虚拟公共析构函数并声明为final。”? 什么是“非抽象可实例化类”?基类吗? - John

32
问题实际上是,你是否想要强制规定如何使用你的类?为什么? 如果一个类没有虚析构函数,那么任何使用该类的人都知道不打算从中派生,并且如果尝试这样做会应用什么限制。这不够好吗? 或者你需要编译器在任何敢于做出未预期行为的人面前抛出硬错误吗? 如果你打算让人们从中派生,请给该类一个虚析构函数。否则不要这样做,并假设任何使用你的代码的人都足够聪明以正确使用你的代码。

13
@jalf: 如果它们派生自它并希望以多态方式使用它 - Oszkar

11

不需要!虚析构函数仅在通过基类指针删除派生类对象时使用。如果您的类不打算在此场景中充当基类,则不要将析构函数设置为虚拟的 - 这会发送错误的信息。


那么你如何预测代码有用的每一个情形呢?也许会出现一些情况,你所设计的类在非多态使用时只有在某些非常特定的实例中作为基类才有用。 - v010dya

5

请查看Herb Sutter的这篇文章

指南 #4:基类的析构函数应该是公有的和虚拟的,或者是受保护的和非虚拟的。


9
请注意,他谈论的是旨在作为“基类”的类,而这个问题特别涉及不设计为基类的类。 - Motti
@Paolo Tedesco说:“应该是public和virtual,或者protected和nonvirtual,或者public和non-virtual并标记为final关键字。”我说得对吗? - John
@John 这个答案比 C++11 更早 :) - Paolo Tedesco

2

对于这个普遍的问题,我的回答是“不需要”。并不是每个类都需要一个。如果你能确定这个类永远不需要被继承,那么就没有必要增加额外的开销。但是,如果有可能会被继承,为了保险起见,最好还是加上一个。


1
基类包含至少一个纯虚函数时,它变成抽象类。如果基类没有虚析构函数,而派生类(派生自基类)有,则可以通过派生类指针安全地销毁派生对象,但不能通过基类指针进行。

0

我要补充的是,有时候当我在父类或子类中忘记了虚函数时,析构函数没有被调用,这让我感到困惑了一段时间。不过现在我知道该注意这个问题了。 :)

有人可能会争辩说,在父类的析构函数中做某些事情,而子类不应该这样做...但这可能表明你的继承结构存在问题。


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