11.3.5 虚析构函数
虽然构造函数不能被定义成虚函数,但析构函数可以定义为虚函数。通常情况下如果类中定义了虚函数,析构函数也应被定义为虚析构函数,尤其是类内有申请的动态内存,需要清理和释放的时候。先来看个简单的例子,如代码11.10所示。
代码11.10 析构函数调用不当带来的内存泄露ImproperDestructor
<————————————-文件名:example1110.cpp——————————————> 01 #include<iostream> 02 using namespace std; 03 class Base//基类定义 04 { 05 private://字符指针 06 char*data; 07 public: 08 Base()//无参构造函数 09 { 10 data=new char[64];//动态内存申请 11 cout<<"Base类构造函数被调用"<<endl; 12 }; 13 ~Base()//析构函数 14 { 15 delete[]data;//data指向的内存被释放 16 cout<<"Base类析构函数被调用"<<endl; 17 }; 18 }; 19 class Child:public Base//Child类由基类Base派生而来 20 { 21 private: 22 char*m_data;//增添的字符指针成员 23 public: 24 Child():Base()//构造函数,初始化表中执行基类的构造函数 25 { 26 m_data=new char[64];//动态申请内存,并将首地址赋给m_data 27 cout<<"Child类构造函数被调用"<<endl; 28 }; 29 ~Child()//析构函数 30 { 31 delete[]m_data;//内存资源释放 32 cout<<"Child类析构函数被调用"<<endl; 33 }; 34 }; 35 int main() 36 { 37 Base*pB=new Child;//动态申请了一块Child大小的内存,赋给Base基类指针 38 delete pB;//执行基类析构函数 39 return 0; 40 }
输出结果如下所示。
Base类构造函数被调用
Child类构造函数被调用
Base类析构函数被调用
【代码解析】编译链接,程序没有任何错误,但细心的读者可能早已发现,代码11.10的语句存在内存泄露问题,main函数中语句“delete pB;”根据指针pB的类型决定应调用哪个类的析构函数,即编译器会调用Base类的析构函数,这意味着Child类的析构函数没有被调用,Child类中char型指针m_data指向的动态内存泄露。
将基类中的析构函数定义为虚析构函数可很好地解决这一问题,为了便于说明,我们用代码11.10的Child类为基础再派生一个GrandChild类,如代码11.11所示。
代码11.11 使用虚析构函数解决内存泄露问题VirtualDestructor
<————————————-文件名:example1111.cpp——————————————> 01 #include<iostream> 02 using namespace std; 03 class Base//基类定义 04 { 05 private: 06 char*data;//字符指针 07 public: 08 Base()//无参构造函数 09 { 10 data=new char[64];//动态内存申请 11 cout<<"Base类构造函数被调用"<<endl; 12 }; 13 virtual~Base()//虚析构函数 14 { 15 delete[]data;//data指向的内存被释放 16 cout<<"Base类析构函数被调用"<<endl; 17 }; 18 }; 19 class Child:public Base//Child类由基类Base派生而来 20 { 21 private: 22 char*m_data;//增添的字符指针成员 23 public: 24 Child():Base()//构造函数,初始化表中执行基类的构造函数 25 { 26 m_data=new char[64];//动态申请内存,并将首地址赋给m_data 27 cout<<"Child类构造函数被调用"<<endl; 28 }; 29 ~Child()//析构函数,继承虚拟virtual 30 { 31 delete[]m_data;//内存资源释放 32 cout<<"Child类析构函数被调用"<<endl; 33 }; 34 }; 35 class GrandChild:public Child//GrandChild类由Child类派生而来 36 { 37 private: 38 char*mm_data;//在Child类基础上增加的字符指针成员mm_data 39 public: 40 GrandChild()//构造函数 41 { 42 mm_data=new char[64];//动态内存申请 43 cout<<"GrandChild类构造函数被调用"<<endl; 44 }; 45 ~GrandChild()//虚析构函数,virtual从继承结构中得来 46 { 47 delete[]mm_data;//内存释放 48 cout<<"GrandChild类析构函数被调用"<<endl; 49 }; 50 }; 51 int main() 52 { 53 Base*pB=new Child;//动态申请了一块Child大小的内存,赋给Base基类指针 54 delete pB;//Child类的析构函数执行,释放内存,不会泄露 55 56 Child*pC=new GrandChild;//动态申请了一块GrandChild大小的内存,赋给Child类指针 57 delete pC;//执行GrandChild类的析构函数,释放内存,不会泄露 58 return 0; 59 }
输出结果如下所示。
Base类构造函数被调用
Child类构造函数被调用
Child类析构函数被调用
Base类析构函数被调用
Base类构造函数被调用
Child类构造函数被调用
GrandChild类构造函数被调用
GrandChild类析构函数被调用
Child类析构函数被调用
Base类析构函数被调用
【代码解析】从代码11.11的输出结果可以看出,代码第54行“delete pB;”和代码第57行“delete pC;”的操作很好地调用了类层次结构上的所有析构函数,避免了内存泄露,关键是在Base类中析构函数用virtual修饰,定义为了虚析构函数,这意味着从该基类的所有派生类的析构函数都继承为虚函数,比如代码11.11中Child类和GrandChild类的析构函数,通过“delete指针”撤销指针所指的对象时,不再根据指针的类型,而是根据指针指向对象的类型来调用析构函数,很好地完成了内存清理的任务。
注意
虚析构函数的继承与普通虚函数的继承其函数名不同,除了“~”外,析构函数和类同名,所以只要基类的析构函数定义为虚函数,派生类中的析构函数便继承为虚函数。