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

有没有一个很好的理由申报一个类的虚拟析构函数? 你应该什么时候专门避免写一个?

当下列任何一个条件成立时,不需要使用虚拟析构函数:

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

没有具体的理由来避免它,除非你真的如此紧张的记忆。

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

C ++ '98 / '03

添加虚拟析构函数可能会将您的类从POD(普通旧数据) *或聚合更改为非POD。 这可以阻止你的项目编译,如果你的类types是聚合初始化的地方。

struct A { // virtual ~A (); int i; int j; }; void foo () { A a = { 0, 1 }; // Will fail if virtual dtor declared } 

在极端的情况下,这种改变也可能导致未定义的行为,其中类以需要POD的方式使用,例如通过省略号parameter passing,或者在memcpy中使用。

 void bar (...); void foo (A & a) { bar (a); // Undefined behavior if virtual dtor declared } 

[* PODtypes是一种对其内存布局有特定保证的types。 该标准确实只是说,如果你要从PODtypes的对象复制到一个字符(或无符号字符)数组,然后再返回,那么结果将与原始对象相同。

现代C ++

在最近的C ++版本中,POD的概念在类的布局和构造,复制和销毁之间被分开。

对于省略号的情况,它不再是undefined behavior ,而是现在conditionally-supported implementation-defined semantics (N3937 – 〜C ++ '14 – 5.2.2 / 7):

…通过具有非平凡复制构造函数,非平凡移动构造函数或重要析构函数的类types(第9章)的潜在评估参数(没有相应的参数)是有条件地支持的,定义的语义。

声明一个析构函数而不是=default将意味着它不是微不足道的(12.4 / 5)

如果不是用户提供的,析构函数是微不足道的…

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

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

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

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

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

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

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

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

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

在所有其他情况下,您将自己节省debugging苦难,使虚拟化。

并非所有的C ++类都适合用作具有dynamic多态性的基类。

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

如果你的类不适合dynamic多态,那么析构函数就不应该被标记为虚拟的,因为这样做是误导性的。 它只是鼓励人们错误地使用你的课程。

下面是一个不适合dynamic多态的类的例子,即使它的析构函数是虚的:

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

不把析构函数声明为虚函数的一个很好的理由是当这个类从添加虚函数表中保存时,你应该尽可能地避免这种情况。

我知道很多人喜欢总是将析构函数声明为虚函数,只是为了安全起见。 但是如果你的class级没有任何其他的虚拟function,那么虚拟析构函数确实没有任何意义。 即使你将课堂授予其他人,然后从其他人那里得到其他的课程,他们也没有理由对你的课堂上的指针调用删除 – 如果他们这样做,那么我会认为这是一个错误。

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

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

如果你有一个非常小的类和大量的实例,一个vtable指针的开销可以在你的程序的内存使用率上有所不同。 只要你的类没有任何其他的虚拟方法,使析构函数非虚拟将节省开销。

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

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

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

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

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

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

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

performance的答案是我所知道的唯一一个可能是真实的。 如果你已经测量并发现将你的析构函数去虚拟化可以加快速度,那么你可能在这个类中还有其他的东西需要加速,但是在这一点上还有更重要的考虑。 有一天有人会发现你的代码会为他们提供一个很好的基类,并为他们保存一周的工作。 你最好确保他们能够完成那一周的工作,复制和粘贴你的代码,而不是将你的代码作为基础。 你最好确保你的一些重要的方法是私人的,这样任何人都不能从你那里inheritance。