据我所知,任何被指定为有子类的类都应该声明为虚析构函数,这样当通过指针访问它们时,类实例可以被正确地销毁。
但是为什么可以声明非虚析构函数的类呢?我相信编译器可以决定何时使用虚析构函数。那么,这是C++设计上的疏忽还是我漏掉了什么?
据我所知,任何被指定为有子类的类都应该声明为虚析构函数,这样当通过指针访问它们时,类实例可以被正确地销毁。
但是为什么可以声明非虚析构函数的类呢?我相信编译器可以决定何时使用虚析构函数。那么,这是C++设计上的疏忽还是我漏掉了什么?
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%的误报率,但"晚了"(可能多次警告)。
C++的设计原则是“一分钱一分货”。 如果您想要某些东西是虚拟的,必须明确地要求。 每个类中的虚函数都必须明确声明(除非它覆盖了基类版本)。
如果具有虚成员的类的析构函数自动设置为虚函数,那么如果您希望使其成为非虚函数,您将如何选择? C++没有能力显式地声明方法为非虚函数。 那么您将如何覆盖此由编译器驱动的行为。
虚类具有非虚析构函数的特定有效用例吗? 我不知道。 也许某个地方有一个退化案例。 但是,如果您出于某种原因需要它,则无法在您的建议下说出它。
当一个类毕竟是非虚拟的时候(注意1),非虚析构函数似乎很有意义。
然而,我并没有看到其他好用处来使用非虚析构函数。
我很欣赏这个问题。非常有趣的问题!
编辑:
注意1:在性能关键的情况下,使用没有任何虚函数表的类可能是有利的,因此根本没有任何虚析构函数。
例如:想想只包含三个浮点值的class Vector3
。 如果应用程序存储它们的数组,则该数组可以以紧凑的方式存储。
如果我们需要虚函数表,并且如果我们甚至需要在堆上进行存储(如Java&co.),则该数组将仅包含指向实际元素“SOMEWHERE”的指针。
编辑2:
我们甚至可能具有没有任何虚方法的类继承树。
为什么?
因为即使“具有“虚拟”方法似乎是常见和可取的情况”,但它并不是我们-人类-所能想象的唯一情况。
和这门语言的许多细节一样,C++ 提供了选择。你可以选择其中一个提供的选项,通常你会选择其他人选择的选项。但有时候你不想要那个选项!
在我们的例子中,一个名为 Vector3 的类可以继承自 Vector2 类,并且仍然不需要虚函数调用的开销。尽管如此,那个例子并不是很好 ;)
我在这里没有看到提到的另一个原因是DLL边界:您希望使用相同的分配器释放对象,以便与用于分配它的分配器相同。
如果方法存在于DLL中,但客户端代码使用直接new
实例化对象,则客户端的分配器用于获取对象的内存,但对象填充了来自DLL的vtable,该vtable指向使用DLL链接的分配器来释放对象的析构函数。
当在客户端中从DLL子类化类时,问题就消失了,因为不使用DLL中的虚拟析构函数。
operator delete
,那么通过基类指针销毁对象的代码就不知道这一点,因此你要么发明一个机制让析构函数返回内存是否已经被释放,要么让析构函数直接调用释放函数。G++ 和 MSVC 都采用后者。 - Simon Richter
virtual
函数,那么为什么我们 仍然需要 将析构函数声明为虚函数?为什么编译器不能自动将其视为虚函数? - Nawaz