103

是否有充分的理由为类声明虚拟析构函数?你什么时候应该特别避免写一个?

4

12 回答 12

76

当以下任何一种情况为真时,无需使用虚拟析构函数:

  • 无意从中派生类
  • 堆上没有实例化
  • 无意通过指向超类的指针访问存储

除非您真的非常需要记忆,否则没有具体的理由避免它。

于 2008-11-19T04:33:10.410 回答
71

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

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
}
于 2008-11-19T15:09:55.740 回答
29

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

于 2008-11-19T04:37:54.440 回答
7

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

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

如果您的代码不是性能关键,那么为您编写的每个基类添加一个虚拟析构函数是合理的,只是为了安全。

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

于 2008-11-19T04:54:35.297 回答
5

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

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

在所有其他情况下,您将省去调试痛苦以使 dtor 虚拟化。

于 2008-11-19T15:31:29.597 回答
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 坐在堆栈上。如果您要传递指向此类对象的指针,更不用说它的子类了,那么您做错了。

于 2008-11-21T14:09:30.807 回答
4

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

我知道很多人更喜欢总是将析构函数声明为虚拟的,只是为了安全起见。但是,如果您的类没有任何其他虚函数,那么拥有虚析构函数真的非常没有意义。即使您将您的类提供给其他人,然后他们从中派生出其他类,那么他们也没有理由对向上转换到您的类的指针调用 delete - 如果他们这样做,那么我会认为这是一个错误。

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

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

于 2016-02-17T07:12:30.203 回答
3

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

于 2010-04-12T23:19:29.223 回答
1

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

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

于 2008-11-19T10:52:30.847 回答
1

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

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

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

于 2011-05-11T22:34:19.523 回答
0

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

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

于 2010-11-13T17:52:49.080 回答
-8

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

于 2008-11-19T05:04:18.490 回答