20

据我所知,任何被指定为具有子类的类都应使用虚拟析构函数声明,因此在通过指针访问它们时可以正确销毁类实例。

但是为什么甚至可以用非虚拟析构函数声明这样的类呢?我相信编译器可以决定何时使用虚拟析构函数。那么,这是 C++ 设计疏忽,还是我遗漏了什么?

4

5 回答 5

19

使用非虚拟析构函数有什么具体原因吗?

是的,有。

主要归结为性能。不能内联虚函数,相反,您必须首先确定要调用的正确函数(这需要运行时信息),然后再调用该函数。

在对性能敏感的代码中,无代码和“简单”函数调用之间的区别可能会有所不同。与许多语言不同,C++ 并不认为这种差异是微不足道的。

但是为什么甚至可以用非虚拟析构函数声明这样的类呢?

因为很难(对于编译器)知道该类是否需要虚拟析构函数。

在以下情况下需要虚拟析构函数:

  • 你调用delete一个指针
  • 通过基类到派生对象

当编译器看到类定义时:

  • 它不知道你打算从这个类派生——你毕竟可以从没有虚拟方法的类派生
  • 但更令人生畏:它不知道你打算delete在这个类上调用

许多人认为多态性需要更新实例,这只是缺乏想象力:

class Base { public: virtual void foo() const = 0; protected: ~Base() {} };

class Derived: public Base {
  public: virtual void foo() const { std::cout << "Hello, World!\n"; }
};

void print(Base const& b) { b.foo(); }

int main() {
  Derived d;
  print(d);
}

在这种情况下,不需要为虚拟析构函数付费,因为在销毁时不涉及多态性。

最后,这是一个哲学问题。在可行的情况下,C++ 默认选择性能和最少的服务(主要的例外是 RTTI)。


关于警告。有两个警告可以用来发现问题:

  • -Wnon-virtual-dtor(gcc, Clang):只要具有虚函数的类没有声明虚析构函数,就会发出警告,除非基类中的析构函数是 make protected。这是一个悲观的警告,但至少你不会错过任何事情。

  • -Wdelete-non-virtual-dtor(Clang,也移植到 gcc):每当在指向具有虚函数但没有虚析构函数的类的指针上调用时都会发出警告delete,除非该类被标记final。它的误报率为 0%,但会发出“迟到”的警告(并且可能会发出多次警告)。

于 2012-01-02T07:39:46.457 回答
3

为什么析构函数默认不是虚拟的? http://www2.research.att.com/~bs/bs_faq2.html#virtual-dtor

准则 #4:基类析构函数应该是公共的和虚拟的,或者是受保护的和非虚拟的。 http://www.gotw.ca/publications/mill18.htm

另见:http ://www.erata.net/programming/virtual-destructors/

编辑:可能重复?什么时候不应该使用虚拟析构函数?

于 2012-01-02T06:56:55.730 回答
2

你的问题基本上是这样的,“如果类有任何虚拟成员,为什么 C++ 编译器不强制你的析构函数是虚拟的?” 这个问题背后的逻辑是,应该将虚拟析构函数与它们打算从中派生的类一起使用。

C++ 编译器不试图超越程序员的原因有很多。

  1. C++ 的设计原则是物有所值。如果你想要一些虚拟的东西,你必须要求它。明确的。虚拟类中的每个函数都必须显式声明(除非它覆盖基类版本)。

  2. 如果具有虚拟成员的类的析构函数自动设为虚拟,那么如果这是您想要的,您将如何选择使其成为非虚拟?C++ 没有能力显式声明非虚拟方法。那么你将如何覆盖这种编译器驱动的行为。

    具有非虚拟析构函数的虚拟类是否有特定的有效用例?我不知道。也许某处有一个退化的案例。但是如果你出于某种原因需要它,你将无法在你的建议下说出来。

您真正应该问自己的问题是,当具有虚拟成员的类没有虚拟析构函数时,为什么更多编译器不会发出警告。毕竟,这就是警告的用途。

于 2012-01-02T06:55:27.563 回答
1

当一个类毕竟是非虚拟的时,非虚拟析构函数似乎是有意义的(注 1)。

但是,我没有看到非虚拟析构函数有任何其他好的用途。

我很欣赏这个问题。非常有趣的问题!

编辑:

注意 1:在性能关键的情况下,使用没有任何虚函数表的类可能是有利的,因此根本没有任何虚析构函数。

例如:考虑class Vector3仅包含三个浮点值的 a。如果应用程序存储了它们的数组,那么该数组可以以紧凑的方式存储。

如果我们需要一个虚函数表,并且如果我们甚至需要在堆上存储(如 Java & co.),那么该数组将只包含指向内存中实际元素“SOMEWHERE”的指针。

编辑2:

我们甚至可能有一个完全没有任何虚拟方法的类的继承树。

为什么?

因为,即使拥有“虚拟”方法似乎是常见且可取的情况,但这并不是我们——人类——可以想象的唯一情况。

与该语言的许多细节一样,C++ 为您提供了选择。您可以选择提供的选项之一,通常您会选择其他人选择的选项。但有时你不想要那个选项!

在我们的示例中,类 Vector3 可以从类 Vector2 继承,并且仍然没有虚函数调用的开销。想了想,那个例子不是很好;)

于 2012-01-02T06:08:18.600 回答
1

我在这里没有提到的另一个原因是 DLL 边界:您想使用相同的分配器来释放您用来分配它的对象。

如果方法存在于 DLL 中,但客户端代码使用 direct 实例化对象new,则客户端的分配器用于获取对象的内存,但该对象使用 DLL 中的 vtable 填充,该 vtable 指向使用 DLL 链接的分配器来释放对象的析构函数。

在客户端中从 DLL 子类化类时,由于未使用 DLL 中的虚拟析构函数,问题就消失了。

于 2012-01-02T07:28:28.643 回答