据我所知,任何指定具有子类的类都应该使用虚拟析构函数声明,因此通过指针访问类实例时可以正确销毁它们。
但为什么甚至可以用非虚析构函数声明这样的类呢?我相信编译器可以决定何时使用虚拟析构函数。那么,这是C ++设计的疏忽,还是我错过了什么?
答案 0 :(得分:18)
使用非虚拟析构函数是否有任何特定原因?
是的,有。
主要是归结为性能。无法内联虚函数,而必须首先确定要调用的正确函数(这需要运行时信息),然后调用该函数。
在对性能敏感的代码中,无代码和“简单”函数调用之间的区别可能会有所不同。与许多语言不同,C ++并不认为这种差异是微不足道的。
但为什么甚至可以用非虚析构函数声明这样的类?
因为很难知道(对于编译器)类是否需要虚拟析构函数。
在以下情况下需要虚拟析构函数:
delete
当编译器看到类定义时:
delete
许多人认为多态性需要 newing 这个实例,这只是缺乏想象力:
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):只要具有虚函数的类没有声明虚析构函数,就会发出警告,除非基类中的析构函数是protected
。这是一个悲观的警告,但至少你不会错过任何事情。
-Wdelete-non-virtual-dtor
(Clang,也移植到gcc ):在指向具有虚函数但没有虚析构函数的类的指针上调用delete
时发出警告,除非该类标记为final
。它的误报率为0%,但警告“迟到”(可能是几次)。
答案 1 :(得分: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/
编辑:可能重复? When should you not use virtual destructors?答案 2 :(得分:2)
你的问题基本上是这样的,“如果类有任何虚拟成员,为什么C ++编译器不强制你的析构函数是虚拟的?”这个问题背后的逻辑是,人们应该使用虚拟析构函数和他们想要派生的类。
C ++编译器没有试图超越程序员的原因有很多。
C ++的设计原则是获得您所支付的费用。如果你想要虚拟的东西,你必须要求它。明确。必须显式声明类中的每个虚函数(除非它覆盖基类版本)。
如果具有虚拟成员的类的析构函数自动变为虚拟,那么如果您想要的话,您将如何选择非虚拟? C ++没有能力显式声明非虚方法。那么你将如何覆盖这种编译器驱动的行为。
对于具有非虚拟析构函数的虚拟类,是否存在特定的有效用例?我不知道。也许在某个地方有一个堕落的案例。但是如果你出于某种原因需要它,你就无法根据你的建议说出来。
您应该问自己的问题是,当具有虚拟成员的类没有虚拟析构函数时,更多编译器不会发出警告。毕竟这就是警告。
答案 3 :(得分:1)
非虚拟析构函数似乎有意义,毕竟一个类毕竟是非虚拟的(注1)。
但是,我没有看到任何其他用于非虚拟析构函数的好用。
我很欣赏这个问题。非常有趣的问题!
修改强> 的
注1: 在性能关键的情况下,使用没有任何虚函数表的类可能是有利的,因此根本没有任何虚拟析构函数。
例如:考虑一个只包含三个浮点值的class Vector3
。如果应用程序存储了它们的数组,那么该数组可以以紧凑的方式存储。
如果我们需要一个虚函数表,如果我们甚至需要在堆上存储(如在Java&amp; co。中),那么该数组将只包含指向内存中“SOMEWHERE”实际元素的指针。
编辑2:
我们甚至可能有一个类的继承树,根本没有任何虚拟方法。
为什么?
因为,即使拥有“虚拟”方法似乎是常见且优选的情况,它也不是我们 - 人类 - 可以想象的唯一情况。
正如该语言的许多细节一样,C ++为您提供了一个选择。您可以选择其中一个提供的选项,通常您会选择其他人选择的选项。但有时候你不想要这个选择!
在我们的示例中,类Vector3可以从类Vector2继承,但仍然没有虚函数调用的开销。以为,这个例子不是很好;)
答案 4 :(得分:1)
我在这里没有提到的另一个原因是DLL边界:你想使用相同的分配器来释放你用来分配它的对象。
如果方法存在于DLL中,但客户端代码使用直接new
实例化对象,则客户端的分配器用于获取对象的内存,但该对象用vtable填充来自DLL,它指向一个析构函数,它使用DLL链接的分配器来释放对象。
当从客户端的DLL中继承类时,问题就会消失,因为不使用DLL中的虚析构函数。