[Effective C++ --007]为多态基类声明virtual析构函数
引言:
我们都知道类的一个很明显的特性是多态,比如我们声明一个水果的基类:
class Fruit { public: Fruit() {}; ~Fruit(){}; }
那么我们根据这个Fruit基类可以派生出以下的子类:
class Apple:public Fruit{}; class Orange:public Fruit{};
那么问题来了,如果我们想经由一个基类指针去删除一个派生类Apple,且我们有以下的方法
Fruit * foo(){ Apple * p = new Apple(); return p; // 子转父,大丈夫 }
接下来似乎顺理成章,我们只需要完成以下的main函数即可了。
int main() { A *p = foo(); delete p; return 0; }
但是实际运行就会发现:Apple对象并未被删除!
这是为什么呢?因为foo()返回的指针指向一个Apple对象,其中的Apple的成员变量很可能未被销毁,而且Apple的析构函数也未被执行起来。于是造成了一个局部销毁的局面。
一、解决方案
其实很简单,只需要将基类中的析构函数定义为虚函数即可。
class Fruit { public: Fruit() {}; virtual ~Fruit(){}; ← }
这样在删除p的时候,会先调用Apple的析构函数,然后再调用Fruit的析构函数,最后删除整个Apple对象。
二、扩展
在书中所说:
在string等STL容器使用的时候,即时class不带virtual函数,可能还是会被是否为虚函数的问题折腾。比如我们拿string来作为一个基类声明以下的class
#include <iostream> #include<string> using namespace std; class D : public string { public: D(string i) {}; ~D() {}; string i; }; int main() { D* d = new D("DD"); string *p; p = d; delete p; // 书中描写此处会发生内存泄露,但是十分不解,上面的p =d 是子赋给父,按理是不应该出现问题的 return 0; }
调试结果在VS环境下也不会出现错误,不清楚是为什么,还有待调查。
2014.11.11 调查结果:
在上述结果中,确实不会出现问题,因为只是单纯的删除了p,但是d中间的值有没有得到删除就不能得到确定了。
这跟我们删除d的目的是相悖的。
添加以下的代码来做说明:
class D : public string { public: D(string str, int i): s(str), length(i){}; ~D() { cout << "call me"; }; private: string s; int length; }; int main() { D* d = new D("DD", 5); string *p; p = d; delete p; // 此时释放p,并不会调用~D(),因此原本给length赋值了5并不会被清除!!! return 0; }
三、纯虚析构函数的调用顺序
在基类存在纯虚析构函数的时候,析构函数的运作方式是:派生class的析构函数先被调用,然后再调用基类的析构函数。
1 #include <iostream> 2 #include<string> 3 4 using namespace std; 5 6 class A { 7 public: 8 A() {}; 9 virtual ~A() = 0; 10 }; 11 A::~A() { 12 cout << "~A" << endl; 13 } 14 class B : public A { 15 public: 16 B() {}; 17 ~B() { 18 cout << "~B" << endl; 19 } 20 }; 21 22 int main() 23 { 24 B* b = new B(); 25 delete b; 26 27 return 0; 28 }
运行后输出:
~B
~A
■总结:
1.带多态性质的基类应该声明一个virtual析构函数,如果class带有任何的虚函数,那么它就应该拥有一个virtual析构函数。
2.class的设计目的如果不是作为基类使用,或不是为了具备多态性,就不应该声明virtual析构函数。