2008-10-12

Effective C++ 第七条款中讲到: 多态基类声明virtual析构函数

Effective C++ 第七条款中讲到: 多态基类声明virtual析构函数

 

关于这个问题, 我们有一个疑问:

如果父类中不定义虚的析构函数, 而通过父类的指针delete子类的对象, 会怎么样?

 

class Base

{

private:

      int m_iBaseData;

};

 

class Derived : public Base

{

public:

      Derived()    {          m_piDerivedData = new int(3);    }

      ~Derived()  {          delete m_piDerivedData;           }

private:

      int * m_piDerivedData;

};

 

int _tmain(int argc, _TCHAR* argv[])

{

      Base BaseObj;

      Base* pb =  new Derived();

      delete pb;

 

      return 0;

}

 

我觉得这个问题应该分两块来剖析.

1.       C++语法方面,

因为父类没有声明虚函数,所以不会有虚表出现(参见C++对象模型第一章), 进而无法实现多态, 所以子类的析构函数不会被调用到.

有一点我们可以肯定, 因为子类的析构函数没有被调用到, 子类申请的资源将不能通过析构函数释放, 将造成内存泄露.

2.       从内存管理方面

首先, 我们知道CRT进行内存管理过程中, new一块内存时需要提供一个size, 这样delete时才能记住, 先前申请了多大的内存块.参考下面一段话:

 

摘自: http://topic.csdn.net/t/20061104/21/5133631.html

这里有一个问题,就是当我们调用   new   operator   分配内存时,有一个   size   参数表明需要分配多大的内存。但是当调用   delete   operator   时,却没有类似的参数,那么   delete   operator   如何能够知道需要释放该指针指向的内存块的大小呢?答案是:对于系统自有的数据类型,语言本身就能区分内存块的大小,而对于自定义数据类型(如我们自定义的类),则   operator   new     operator   delete   之间需要互相传递信息。   
    
 
  当我们使用   operator   new   为一个自定义类型对象分配内存时,实际上我们得到的内存要比实际对象的内存大一些,这些内存除了要存储对象数据外,还需要记录这片内存的大小,此方法称为   cookie.这一点上的实现依据不同的编译器不同。(例如   MFC   选择在所分配内存的头部存储对象实际数据,而后面的部分存储边界标志和内存大小信息。g++   则采用在所分配内存的头   4   个自己存储相关信息,而后面的内存存储对象实际数据。)当我们使用   delete   operator   进行内存释放操作时,delete   operator   就可以根据这些信息正确的释放指针所指向的内存块。   
    
 
  以上论述的是对于单个对象的内存分配/释放,当我们为数组分配/释放内存时,虽然我们仍然使用   new   operator     delete   operator,但是其内部行为却有不同:new   operator   调用了operator   new   的数组版的兄弟-   operator   new[],而后针对每一个数组成员调用构造函数。而   delete   operator   先对每一个数组成员调用析构函数,而后调用   operator   delete[]   来释放内存。需要注意的是,当我们创建或释放由自定义数据类型所构成的数组时,编译器为了能够标识出在   operator   delete[]   中所需释放的内存块的大小,也使用了编译器相关的   cookie   技术。

 

我们来做一个实验:

#include "stdafx.h"

#include <iostream>

using namespace std;

 

class Base

{

public:

   Base() { m_iX = 2; m_iY = 3; }

private:

   int m_iX;

   int m_iY;

};

 

int _tmain(int argc, _TCHAR* argv[])

{

   Base* pb =  new Base();

   int * int_pointer_to_base = (int*) pb; //Base类的指针转成int*

 

   delete int_pointer_to_base;

   return 0;

}

   设置断点: 在执行delete int_pointer_to_base;之前, 内存状况如图1所示:

从图中看到int_pointer_to_base实际上是指向了m_iX的位置.

F10再走一步, 当执行完delete 操作后, 内存如下图2所示:

我们看到delete, m_iX, m_iY内存变成了0xfeeefeee(Debug模式下, VC编译器自动将delete掉的内存块, 填充上0xfeee), 虽然Release模式下, m_iXm_iY的内存不会被填充0xfeee, 但是我们已经知道, 内存管理中已经将申请时的大小sizeof(Base)回收了.

 

结论:

    Base BaseObj;

      Base* pb =  new Derived();

      delete pb;

 

通过父类指针来deletenew出来的子类的对象, 对于子类对象的内存区域中(继承中子类实例内存部局, 请参考C++对象模型), 不仅父类的部分会被回收, 子类的部分也会被回收.

 



图1:
图2: