什么情况下不应该使用虚析构函数?

108

在一个类中有没有声明虚析构函数有时候是有好处的,但是否需要特意避免编写虚析构函数呢?

12个回答

82

下列情况之一为真时,无需使用虚析构函数:

  • 没有意图从它派生类
  • 不在堆上创建实例
  • 没有意图通过超类指针访问存储

除非你真的特别需要节省内存,否则没有特定的理由避免使用虚析构函数。


31
这不是一个好的回答。“没有必要”与“不应该”不同,“没有意图”与“不可能实现”也不同。 - Windows programmer
5
添加:没有通过基类指针删除实例的意图。 - Adam Rosenfield
9
这并没有真正回答这个问题。你有什么充分的理由不使用虚析构函数呢? - mxcl
10
当没有必要做某事时,不去做是一个很好的理由。这遵循了XP的简单设计原则。 - sep
14
说“没有意图”,你对于你的类将如何被使用做了一个巨大的假设。在我看来,大多数情况下最简单的解决方案(因此应该是默认的)应该是拥有虚析构函数,只有在你有特定原因不需要时才避免使用它们。因此,我仍然很想知道什么是一个好的理由。 - ckarras
显示剩余6条评论

75
为了直接回答这个问题,即何时不应声明虚拟析构函数。
C++ '98/'03
添加虚拟析构函数可能会将您的类从POD (plain old data)*或聚合体更改为非POD。如果您的类类型在某处进行了聚合初始化,则可能会导致项目无法编译。
struct A {
  // virtual ~A ();
  int i;
  int j;
};
void foo () { 
  A a = { 0, 1 };  // Will fail if virtual dtor declared
}

在极端情况下,这种更改也可能导致未定义的行为,其中类被用于需要POD的方式,例如通过省略号参数传递它,或者使用memcpy。
void bar (...);
void foo (A & a) { 
  bar (a);  // Undefined behavior if virtual dtor declared
}

[* POD类型是一种具有特定内存布局保证的类型。标准实际上只是说,如果您从具有POD类型的对象复制到char(或unsigned char)数组中,然后再次复制回来,则结果将与原始对象相同。] 现代C ++ 在最近的C ++版本中,POD的概念被分为类布局及其构建,复制和销毁。
对于省略号情况,它不再是未定义的行为,而是具有实现定义语义的条件支持(N3937 〜C ++'14-5.2.2 / 7):
“...传递具有非平凡复制构造函数,非平凡移动构造函数或非平凡析构函数的类类型(第9条)的可能计算参数,没有对应的参数,具有实现定义的语义。”
除了 =default 之外声明析构函数将意味着它不是平凡的(12.4/5)
“...如果未提供用户的析构函数,则析构函数是平凡的......”
现代C ++ 的其他变化减少了聚合初始化问题的影响,因为可以添加构造函数:
struct A {
  A(int i, int j);
  virtual ~A ();
  int i;

  int j;
};
void foo () { 
  A a = { 0, 1 };  // OK
}

1
你是对的,我错了,性能不是唯一的原因。但这表明我关于其他方面的想法是正确的:类的程序员最好包括代码,以防止其他人继承该类。 - Windows programmer
亲爱的理查德,你能否再多解释一下你所写的内容。我不太明白你的观点,但这似乎是我通过谷歌搜索找到的唯一有价值的观点。或者你可以给一个更详细的解释链接吗? - John Smith
2
@JohnSmith 我已经更新了答案。希望这能有所帮助。 - Richard Corden

30

只有在我有虚拟方法的情况下,才会声明虚拟析构函数。一旦我有虚拟方法,我就不相信自己可以避免在堆上实例化它或存储基类的指针。这两种操作非常常见,如果析构函数没有声明为虚拟函数,它们通常会默默地泄漏资源。


3
实际上,gcc有一个警告选项,可以在出现这种情况(存在虚拟方法但没有虚拟析构函数)时发出警告。 - CesarB
6
如果您从这个类派生,无论您是否拥有其他虚函数,都会面临内存泄漏的风险。请注意不改变原意,使语言通俗易懂。 - Mag Roader
1
我同意mag的观点。虚析构函数和虚方法是两个不同的需求。虚析构函数提供了一个类执行清理(例如删除内存、关闭文件等)的能力,并确保其所有成员的构造函数被调用。 - user48956
@MagRoader 理论上是需要的,但由于只有在将派生对象分配在堆上并将其指针存储(和删除)到基类指针时才会出现问题,所以显而易见的问题是:没有虚函数的情况下该指针有什么用处?我只能看到一种可能性:你仅在“完成”时使用对象来删除资源;在这种情况下,你应该有一个虚析构函数而没有其他方法。 - Hans Olsson

7

只要有可能在子类对象的指针上调用delete函数,就需要一个虚析构函数。这可以确保在运行时正确地调用析构函数,而无需编译器在编译时知道堆上对象的类类型。例如,假设BA的一个子类:

A *x = new B;
delete x;     // ~B() called, even though x has type A*

如果你的代码不是性能关键的,为了安全起见,给你编写的每个基类都添加一个虚析构函数是合理的。

然而,如果你在紧密的循环中delete了很多对象,调用虚函数(即使是空的)的性能开销可能会很大。编译器通常不能内联这些调用,处理器可能难以预测要去哪里。这不太可能对性能产生重大影响,但值得一提。


如果您的代码不是性能关键型的,为了安全起见,在您编写的每个基类中添加虚析构函数是合理的。请在我看到的每个答案中更加强调这一点。 - csguy

5

虚函数意味着每个分配的对象都会通过虚函数表指针增加内存成本。

因此,如果您的程序涉及分配大量某个对象,则值得避免所有虚函数以节省每个对象的额外32位。

在所有其他情况下,使析构函数虚拟将为自己节省调试痛苦。


1
只是挑刺一下,但现在指针通常会是64位而不是32位。 - Head Geek

5

并非所有的C++类都适合作为带有动态多态性的基类。

如果你想让你的类适合于动态多态性,那么它的析构函数必须是虚的。此外,任何子类可能想要覆盖的方法(这可能意味着所有公共方法,以及一些内部使用的受保护方法)都必须是虚的。

如果你的类不适合于动态多态性,则析构函数不应标记为虚的,因为这样做是误导性的。这只会鼓励人们错误地使用你的类。

以下是一个示例,即使其析构函数是虚的,也不适合用作动态多态性的基类:

class MutexLock {
    mutex *mtx_;
public:
    explicit MutexLock(mutex *mtx) : mtx_(mtx) { mtx_->lock(); }
    ~MutexLock() { mtx_->unlock(); }
private:
    MutexLock(const MutexLock &rhs);
    MutexLock &operator=(const MutexLock &rhs);
};

这个类的整个目的是为了在RAII中保持在堆栈上。如果你传递指向这个类对象的指针,更不用说它的子类了,那么你做错了。


3
多态使用并不意味着多态删除。有许多情况下,一个类需要有虚拟方法但没有虚拟析构函数。例如,考虑一个典型的静态定义的对话框,在几乎任何GUI工具包中都会出现。父窗口将销毁子对象,并且它知道每个对象的确切类型,但所有子窗口也可以在许多地方被用于多态,例如命中测试、绘制、获取文本以便语音引擎阅读等无数地方。 - Ben Voigt
4
确实,但是提问者正在询问何时应该特别避免虚析构函数。对于您描述的对话框,虚析构函数是无意义的,但在我看来并不会有害。我不能确定自己永远不需要使用基类指针删除对话框 - 例如,将来我可能希望父窗口使用工厂创建其子对象。因此,这不是 避免 虚析构函数的问题,只是您可能不需要一个。但是,对于不适合派生的类而言,虚析构函数是有害的,因为它会产生误导。 - Steve Jessop

4
一个不将析构函数声明为虚函数的好理由是,这样可以避免给您的类添加虚函数表,尽可能避免这种情况发生。
我知道很多人喜欢总是将析构函数声明为虚函数,只是为了安全起见。但是,如果您的类没有其他的虚函数,那么就真的没有必要有一个虚析构函数。即使您将您的类给其他人,他们从中派生其他类,那么他们也没有理由调用一个向上转型到您的类的指针 delete - 如果他们这样做了,那么我认为这是一个错误。
好吧,只有一个例外,也就是如果您的类被(误)用于执行派生对象的多态删除,那么您或其他人希望知道这需要一个虚析构函数。
换句话说,如果您的类有一个非虚析构函数,那么这是一个非常明确的声明:“不要使用我来删除派生对象!”

3
如果您有一个非常小的类,但实例数量巨大,那么vtable指针的开销可能会对程序的内存使用造成影响。只要您的类没有其他虚拟方法,将析构函数设置为非虚拟的将节省这种开销。

1

如果您绝对必须确保您的类没有虚表,那么您也不能有虚析构函数。

这是一个罕见的情况,但确实会发生。

最熟悉的执行此操作的模式示例是DirectX D3DVECTOR和D3DMATRIX类。这些是类方法而不是函数,为了语法糖,但这些类故意没有虚表,以避免函数开销,因为这些类专门用于许多高性能应用程序的内部循环。


1

我通常会将析构函数声明为虚函数,但是如果您有内循环中使用的性能关键代码,则可能希望避免虚表查找。在某些情况下这可能非常重要,比如碰撞检测。但是,请注意在使用继承时如何销毁这些对象,否则您只会销毁对象的一半。

请注意,如果对象上有任何虚方法,则会为该对象执行虚表查找。因此,如果类中还有其他虚方法,则删除析构函数上的虚说明没有意义。


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