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

时间:2008-11-19 04:27:51

标签: c++ virtual-functions virtual-destructor

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

12 个答案:

答案 0 :(得分:69)

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

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

没有特别的理由要避免它,除非你真的如此紧迫记忆。

答案 1 :(得分:66)

明确回答问题,即你何时声明虚拟析构函数。

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类型的对象复制到一个字符数组(或无符号字符)并再次返回,那么结果将与原始对象相同。]

现代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
}

答案 2 :(得分:25)

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

答案 3 :(得分:6)

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

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

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

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

答案 4 :(得分:5)

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

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

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

答案 5 :(得分: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的堆栈上。如果你传递指向这个类的对象的指针,更不用说它的子类,那么你就是在做错了。

答案 6 :(得分:4)

没有将析构函数声明为虚拟的一个很好的理由是,这样可以节省你的类没有添加虚函数表,并且你应该尽可能避免使用它。

我知道很多人宁愿总是将析构函数声明为虚拟,只是为了安全起见。但是如果你的类没有任何其他虚函数那么真的,真的没有必要使用虚拟析构函数。即使你把你的课提供给其他人然后从中派生出其他类,那么他们就没有理由在你的班级向上调用指针上删除删除 - 如果他们这样做,我会认为这是一个错误。

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

换句话说,如果你的类有一个非虚拟的析构函数,那么这是一个非常清晰的陈述:"不要用我来删除派生对象!"

答案 7 :(得分:3)

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

答案 8 :(得分:1)

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

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

答案 9 :(得分:1)

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

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

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

答案 10 :(得分:0)

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

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

答案 11 :(得分:-7)

表现答案是我所知道的唯一有可能成为真实的人。如果你已经测量过并发现对析构函数进行去虚拟化确实可以加快速度,那么你可能在该类中还有其他需要加速的东西,但是在这一点上还有更重要的考虑因素。有一天,有人会发现你的代码会为他们提供一个很好的基类,并节省他们一周​​的工作。你最好确保他们做那一周的工作,复制和粘贴代码,而不是以你的代码为基础。你最好确保你把一些重要的方法设为私有,以便没有人可以继承你。