是否有充分的理由不为类声明虚拟析构函数?什么时候应该特别避免写一个?
如果满足以下任何条件,则无需使用虚拟析构函数:
无意从中派生类
堆上没有实例化
无意存储在超类的指针中
没有特别的理由要避免它,除非你真的如此紧迫的记忆.
要明确地回答这个问题,你即当应该没有声明虚析构函数.
C++ '98/'03
添加虚拟析构函数可能会将您的类从POD(普通旧数据)*或聚合更改为非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 }
当且仅当我有虚拟方法时,我声明一个虚拟析构函数.一旦我有虚拟方法,我不相信自己避免在堆上实例化它或存储指向基类的指针.这两个都是非常常见的操作,如果未将析构函数声明为虚拟,则通常会以静默方式泄漏资源.
只要有delete
可能在指向具有类类型的子类的对象的指针上调用,就需要虚拟析构函数.这样可以确保在运行时调用正确的析构函数,而编译器不必在编译时知道堆上对象的类.例如,假设B
是以下的子类A
:
A *x = new B; delete x; // ~B() called, even though x has type A*
如果您的代码不是性能关键,那么为您编写的每个基类添加一个虚拟析构函数是合理的,只是为了安全起见.
但是,如果您发现自己delete
在紧密循环中遇到了很多对象,那么调用虚函数(即使是空函数)的性能开销可能会很明显.编译器通常不能内联这些调用,处理器可能很难预测到哪里去.这不太可能对性能产生重大影响,但值得一提.
虚函数意味着每个分配的对象都会通过虚函数表指针增加内存开销.
因此,如果您的程序涉及分配大量的某些对象,则值得避免所有虚函数以便为每个对象保存额外的32位.
在所有其他情况下,您将节省自己的调试痛苦,使dtor虚拟化.
并非所有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的堆栈上.如果你传递指向这个类的对象的指针,更不用说它的子类,那么你就是在做错了.