为说明问题,首先请查看如下代码,并思考一个问题,“以下代码为什么会出现内存泄露?”。
class test { public: test(const char *& bb=0) { b = new char[sizeof(bb)]; strcpy(b,bb); } virtual void a()=0; ~test() { if(b) delete b; b=0; } private: char *b; }; class test1 : public test { public: test1(const char *& bb=0, const char *& cc=0):test(bb) { c=new char[sizeof(cc)]; strcpy(c,cc); } void a(){} ~test1() { if(c ) delete c; c=0; } private: char *c; }; int main() { char *str = ”abc”; char *str2 = ”def”; test *p = new test1(str1,str2); delete p;//内存泄露 return 0; }
分析为什么会内存泄露?
首先给出对象的一种可能的内存布局图(对象布局方式随编译器的不同而不同,不过不会有很大差异)(参考《深度探索C++对象模型》中画法)。
由于test的析构函数不是虚函数,导致回收p时调用的是test的虚构函数而不是test1的析构函数,此时图2指针p所指对象的char *b被释放了,而char *c没有被释放,从而导致一个诡异的“局部销毁”对象,这样便导致了test1中的c的内存没有释放,从而导致内存泄露。
那是不是所有析构函数都应该声明为虚拟的呢?不是。
虚函数导致class生成一个虚表,增加了空间和时间开销。
所以如果class如果被继承,那么就要声明为virtual,否则不要声明为virtual,这是我当时得出的结论,然而这个结论并不完全正确,看看下面的观点(来自于我对三位C++大师的经典著作的摘录和理解)或许能对你有所帮助。
大师观点:
1.andrew在《C++沉思录》中也讨论了虚析构函数。
1)记住虚函数和非虚函数之间的区别只有在下面这种特定的环境下才会体现出来:当使用一个基类指针来指向或引用一个派生类对象时。
2)当有下面两件事情同时发生时就需要虚析构函数了:
a)有需要析构函数的事情发生。比如需要通过指针销毁对象时。(需要析构函数)
b)它发生在这样一种上下文中:指向一个基类的指针或者引用都有一个静态类型,并且实际上都指向一个派生类的对象。(需要虚函数)
2.scott在《Effective C++ 第三版》的条款7(为多态基类声明virtual析构函数)中有精辟的专门论述。最后他给出了两点忠告:
1)polymorphic(带多态性质的)base classes(并不是任何基类)应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
2)classes的设计目的如果不是作为base class使用,或不是为了具备多态性(polymorphically),就不该声明virtual析构函数(考虑到虚函数回导致对象占用空间的增大,调用时间的增加和降低可移植性的缺点)。
andrew的观点中需要虚函数说明类需要多态,而多态一般和继承同时使用,因此也就导出了多态基类。因此scott的“为多态基类声明virtual析构函数”的观点与andrew的观点有异曲同工之妙。
3.lippman在《C++ primer》中给出了虚析构函数的最佳实践:即使虚构函数没有工作要做,继承层次的根类也应该定义一个虚析构函数。按照scott的建议,这里的根类改成多态根类似乎更合适。我觉得lippman的说法可能有一些历史原因在里面或是出于保守的谨慎(考虑到这是一本面向初学者的C++入门书,出于谨慎态度似乎更合适),lippman在《深度探索C++对象模型》中提到三种C++编程范型:程序型,ADT和OO,并没有提到过泛型编程(应该是当时泛型编程还不流行),泛型编程中存在没有使用virtual析构函数的根类,因为它们不是为多态设计的(不是OO设计风格),因此也许lippman给出的虚析构函数的最佳实践是特指在OO下的最佳实践。
注意lippman强调“即使虚构函数没有工作要做”,这是为什么呢,因为你无法预知未来派生类的行为(有些想法应该避免,比如欲将STL容器(string可以作为一种特殊的STL容器)作为多态基类,当然这不可能,你只能作为基类使用,而不会有多态特性,所以这种想法是很危险的),如果一个用户从你的根类派生了一个类,该类需要释放一些资源时,没有虚析构函数情况下使用多态则很有可能会导致内存泄露,所以最安全的做法就是加上虚析构函数。
我觉得andrew说的最直接,scott说的最实用,而lippman说的最安全。