什么时候不应该使用虚拟析构函数?


Answers:


72

如果满足以下任一条件,则无需使用虚拟析构函数:

  • 无意从中派生类
  • 堆上没有实例化
  • 无意存储在超类的指针中

没有特别的原因可以避免它,除非您真的如此被迫占用内存。


25
这不是一个好答案。“没有需要”与“不应该”不同,并且“无意图”与“不可能”不同。
Windows程序员

5
还添加:无意通过基类指针删除实例。
亚当·罗森菲尔德

9
这并不能真正回答问题。您有什么理由不使用虚拟dtor?
mxcl

9
我认为,当不需要做某事时,那是一个不做的很好的理由。它遵循XP的简单设计原则。
9

12
通过说“没有意图”,您正在对如何使用您的班级做出巨大的假设。在我看来,大多数情况下最简单的解决方案(因此应采用默认设置)应该是具有虚拟析构函数,并且仅在有特殊原因时才避免使用它们。因此,我仍然对这将是一个很好的理由感到好奇。
ckarras 2010年

68

要明确地回答这个问题,即什么时候应该声明一个虚析构函数。

C ++ '98 / '03

添加虚拟析构函数可能会将您的类从POD(普通旧数据) * 更改或聚合为非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类型的对象复制到字符数组(或无符号字符)并再次返回,则结果将与原始对象相同。

现代C ++

在最新版本的C ++中,POD的概念被划分为类布局及其构造,复制和销毁。

对于省略号的情况,它不再是未定义的行为,现在已由实现定义的语义有条件地支持(N3937-〜C ++ '14-5.2.2 / 7):

...传递一个具有非平凡的复制构造函数,非平凡的移动构造函数或平凡的析构函数且没有相应参数的类类型的可能求值的参数(第9条),该实现有条件地受到支持-定义的语义。

声明除析构函数以外的其他方法=default将意味着它并非无关紧要(12.4 / 5)

...如果不是用户提供的,则析构函数很简单...

对Modern C ++的其他更改减少了聚合初始化问题的影响,因为可以添加构造函数:

struct A {
  A(int i, int j);
  virtual ~A ();
  int i;

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

1
您是对的,但我错了,性能并不是唯一的原因。但这表明我对其余部分是正确的:类的程序员最好包含代码以防止该类被其他任何人继承。
Windows程序员

亲爱的理查德,您能否请您对您所写的内容发表更多评论。我不理解您的观点,但是这似乎是我通过谷歌搜索找到的唯一有价值的观点。或者也许您可以提供指向更详细说明的链接?
约翰·史密斯,

1
@JohnSmith我已经更新了答案。希望这会有所帮助。
理查德·科登

27

当且仅当我有虚方法时,我才声明虚析构函数。一旦有了虚拟方法,我就不相信自己会避免在堆上实例化它或存储指向基类的指针。这两个都是极其常见的操作,如果析构函数未声明为虚拟的,则通常会静默地泄漏资源。


3
而且,实际上,gcc上有一个警告选项,可以精确警告这种情况(虚拟方法,但没有虚拟dtor)。
CesarB

6
如果您从该类派生,那么您是否冒着泄漏内存的风险,而不管是否还有其他虚函数?
Mag Roader

1
我同意杂志。虚拟析构函数和/或虚拟方法的这种使用是单独的要求。虚拟析构函数使类能够执行清理(例如,删除内存,关闭文件等),还可以确保调用其所有成员的构造函数。
user48956

7

只要有delete可能在指向具有您的类类型的子类的对象的指针上调用它,就需要一个虚拟析构函数。这样可以确保在运行时调用正确的析构函数,而编译器不必在编译时就知道堆上对象的类。例如,假设B是的子类A

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

如果您的代码不是性能至关重要的对象,则出于安全考虑,将合理的析构函数添加到您编写的每个基类中都是合理的。

但是,如果您发现自己delete在一个紧密的循环中使用了许多对象,则调用虚拟函数(甚至是一个空函数)的性能开销可能会很明显。编译器通常不能内联这些调用,并且处理器可能很难预测到哪里。这不太可能对性能产生重大影响,但是值得一提。


“如果您的代码对性能不是很关键,那么为了安全起见,在您编写的每个基类中添加一个虚拟析构函数是合理的。” 我看到的每个答案中都应强调更多内容
csguy

5

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

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

在所有其他情况下,您将避免调试麻烦,从而使dtor虚拟化。


1
只是吹毛求疵,但这些天的指针经常是64位而不是32
头野人

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的堆栈上。如果您要传递指向此类对象的指针,更不用说它的子类了,那么您就做错了。


2
多态使用并不意味着多态删除。一个类有很多用例,它们具有虚拟方法,但没有虚拟析构函数。在几乎任何GUI工具包中,都考虑一个典型的静态定义对话框。父窗口将销毁子对象,并且知道每个子对象的确切类型,但所有子窗口也将在任意多个位置进行多态使用,例如命中测试,绘图,可访问性API(可为文本获取文本)语音引擎等
Ben Voigt 2010年

4
是的,但发问者在询问您何时应特别避免使用虚拟析构函数。对于您描述的对话框,虚拟析构函数是没有意义的,但IMO却无害。我不确定我将永远不需要使用基类指针删除对话框-例如,将来我可能希望父窗口使用工厂创建其子对象。因此,避免使用虚拟析构函数并不是一个问题,只是您可能不愿意拥有一个。但是,不适合派生的类上的虚拟析构函数有害的,因为它具有误导性。
史蒂夫·杰索普

4

不将析构函数声明为虚拟的一个很好的理由是,这可以避免类添加虚拟函数表,并且应尽可能避免这样做。

我知道许多人宁愿总是为了安全起见始终将析构函数声明为虚拟的。但是,如果您的类没有任何其他虚函数,那么拥有虚拟析构函数就没有任何意义。即使您将您的课程提供给其他人,然后再从该课程中派生其他课程,他们也没有理由在对您的课程cast之以鼻的指针上调用delete-如果他们这样做了,我认为这是一个错误。

好的,只有一个例外,即如果您的类被(错误地)用于执行派生对象的多态删除,但是您(或其他人)希望知道这需要一个虚拟析构函数。

换句话说,如果您的类具有非虚拟的析构函数,那么这是一个非常明确的声明:“不要用我来删除派生对象!”


3

如果您的类非常小且实例数量众多,则vtable指针的开销可能会影响程序的内存使用量。只要您的类没有其他任何虚拟方法,使析构函数为非虚拟方法将节省该开销。


1

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

请注意,如果对象上的任何方法是虚拟的,则对该对象进行虚拟表查找。因此,如果类中还有其他虚拟方法,那么删除析构函数上的虚拟规范毫无意义。


1

如果您绝对肯定要确保您的类没有vtable,那么您也必须也没有虚拟析构函数。

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

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


0

将在基类上执行的操作以及应该虚拟运行的操作应该是虚拟的。如果可以通过基类接口多态地执行删除操作,则删除操作必须是虚拟的并且是虚拟的。

如果您不打算从该类派生,则析构函数不必是虚拟的。即使这样做,如果不需要删除基类指针,则受保护的非虚拟析构函数也同样有效


-7

性能答案是我所知道的唯一一个有可能成为现实的答案。如果您已经测量并发现对析构函数进行虚拟化确实可以加快处理速度,那么该类中可能还需要加快处理速度,但是在这一点上,还有一些重要的考虑因素。总有一天,有人会发现您的代码将为他们提供一个不错的基类,并节省了他们一周的工作。您最好确保他们完成那一周的工作,复制并粘贴您的代码,而不要使用您的代码作为基础。您最好确保将一些重要的方法设为私有,这样任何人都无法从您那里继承。


多态性肯定会减慢速度。将其与需要多态而不选择多态的情况进行比较,它的速度甚至会更慢。示例:我们使用RTTI和switch语句在基类析构函数上实现所有逻辑,以清理资源。
9

1
在C ++中,阻止我从您记录的不适合用作基类的类中继承不是您的责任。我有责任谨慎使用继承。当然,除非房屋风格指南另有说明。
史蒂夫·杰索普

1
...仅仅将析构函数虚拟化并不意味着该类必须作为基类正常工作。因此,将其标记为虚拟“只是因为”而不是进行评估,是在写我的代码无法兑现的支票。
史蒂夫·杰索普
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.