代码改变世界

析构函数的浅谈《原创》

2010-12-16 00:12  Rollen Holt  阅读(17982)  评论(2编辑  收藏  举报

显式的调用析构函数是一件非常危险的事情,,我们自己所谓的显式调用析构函数,实际上只是调用了一个成员函数,并没有真正意义上的让对象析构
为了理解这个问题,我们必须首先弄明白堆区栈区的概念。《具体的区别参加我的文章-《堆区和栈区浅谈》 》

堆区(heap —— 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
    栈区(stack—— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。如果对象被建立在堆上,系统就不会自动调用。
     
所以,如果我们在析构函数中有清除堆数据的语句,调用两次意味着第二次会试图清理已经被清理过了的,根本不再存在的数据!这是件会导致运行时错误的问题,并且在编译的时候不会告诉你!
    显式调用的时候,析构函数相当于的一个普通的成员函数

举个例子说明一下:

Fred *p=new Fred();
delete p;//
自动调用p->~Fred
但是如果我们将上一条语句改为:p->~Fred();呢。会出现什么情况呢?
因为显示调用析构函数不会释放Fred对象本身的内存,也就是栈内存,所以不要这么做,记住delete做了2件事情:调用析构函数和回收内存。

编译器隐式调用析构函数,如分配了堆内存,显式调用析构的话引起重复释放堆内存的异常
   
 把一个对象看作占用了部分栈内存,占用了部分堆内存(如果申请了的话),这样便于理解这个问题
   
 系统隐式调用析构函数的时候,会加入释放栈内存的动作(而堆内存则由用户手工的释放)
   
 用户显式调用析构函数的时候,只是单纯执行析构函数内的语句,不会释放栈内存,摧毁对象
    很罕见的例外在于使用布局newie: placement new)的时候,在delete设置的缓存之前

需要显式调用的析构函数,这实在是很少见的情况。

      什么是定位new呢?定位放置new有很多的作用。最简单的用处就是将对象放置在内存中的特殊位置。这是依靠 new表达式部分的指针参数的位置来完成的。
 
 #include <new>        // 必须 #include 这个,才能使用 "placement new"
 #include "Fred.h"     
// class Fred 的声明
 void someCode()
 {
   char memory[sizeof(Fred)]; 
//在内存中创建了一个sizeof(Fred)字节大小的数组,足够放下 Fred 对象。
   void* place = memory; // 创建了一个指向这块内存的首字节的place指针
 
  Fred* f = new(place) Fred();   // Line #1注意看这里。下面有解释。
   // The pointers f and place will be equal
 
   
// ...
 }

Line #1本质上只是调用了构造函数 Fred::Fred(). Fred构造函数中的this指针将等于place。因此返回的 将等于place

建议:万不得已时才使用“placement new语法。只有当你真的在意对象在内存中的特定位置时才使用它。例如,你的硬件有一个内存映象的 I/O计时器设备,并且你想放置一个Clock对象在那个内存位置。

但是这个是很危险的:你要独自承担这样的责任,传递给“placement new操作符的指针所指向的内存区域必须足够大,并且可能需要为所创建的对象进行边界调整。编译器和运行时系统都不会进行任何的尝试来检查你做的是否正确。如果 Fred 类需要将边界调整为4字节,而你提供的位置没有进行边界调整的话,你就会亲手制造一个严重的灾难(如果你不明白边界调整的意思,那么就不要使用placement new语法)。

你还有析构放置的对象的责任。这通过显式调用析构函数来完成:
void someCode()
 {
   char memory[sizeof(Fred)];
   void* p = memory;
   Fred* f = new(p) Fred();
   
// ...
  
 f->~Fred();   // 显式调用定位放置的对象的析构函数
 }

这是显式调用析构函数的唯一时机。
下面给出一些我的建议哈。大家参考一下,欢迎拍砖

      不要对于局部对象显示的调用析构函数,在创建该局部对象的代码块的 处,析构函数会自动被调用。这是语言所保证的;自动发生。没有办法阻止它。而两次调用同一个对象的析构函数,你得到的真是坏的结果!砰!你完蛋了!
但是也会有一些时候,我们想要将一个局部对象在被创建的代码块 } 之前被析构的话,举个例子吧。比如假设析构 File 对象的作用是关闭文件。现在假定你有一个 File 类的对象 f,并且你想 File  对象的作用范围结束(也就是 )之前被关闭:
Void function
{
 File f
   
// ... [这些代码在 打开的时候执行] ...
   
// <— 希望在此处关闭 f
   
// ... [这些代码在 f 关闭后执行] ...
}
这个时候我们可以人为的添加一个{…}.将局部对象的生命周期长度包裹于这个人为的{…}中就可以了。这样在我们人为的 } 的后面析构函数将会被自动调用。绝大多数的时候我们是可以人为的将局部对象包裹于这个人为的{…}中的,来限制其生命周期。但是也有一些特殊的情况使得我们不能人为的用{…}去包裹一些代码,这个时候我们可以通过增加一个类似于析构函数的成员函数来实现类似的功能。但是我们千万不要调用析构函数本身。
    
 例如,File类的情况下,可以添加一个close()方法。典型的析构函数只是调用close()方法。注意close()方法需要标记 File 对象,以便后续的调用不会再次关闭一个已经关闭的文件。举例来说,可以将一个fileHandle_数据成员设置为 -1,并且在开头检查fileHandle_是否已经等于-1

 class File {
 public:
   void close();
   ~File();
   
// ...
 private:
   int fileHandle_;   
// 当且仅当文件打开时fileHandle_ >= 0
 };
 
 File::~File()
 {
   close();
 }
 
 void File::close()
 {
   if (fileHandle_ >= 0) {
     
// ... [执行一些操作-系统调用来关闭文件] ...
     fileHandle_ = -1;
   }
 }

注意其他的 File方法可能也需要检查fileHandle_是否为 -1(也就是说,检查文件是否被关闭了)。还要注意任何没有实际打开文件的构造函数,都应该将fileHandle_设置为 -1
并且我们在编写析构函数的时候,并不需要显示的调用成员对象的析构函数(排除place new情况)。因为类的析构函数(不论你是否显式地定义了)自动调用成员对象的析构函数。它们以出现在类声明中的顺序的反序被析构。
class Member {
 public:
   ~Member();
   
// ...
 };
 
 class Fred {
 public:
   ~Fred();
   
// ...
 private:
   Member x_;
   Member y_;
   Member z_;
 };
 
 Fred::~Fred()
 {
   
// 编译器自动调用 z_.~Member()
   
// 编译器自动调用 y_.~Member()
   
// 编译器自动调用 x_.~Member()
 }


我们在书写派生类的析构函数的时候,也不需要显示的调用基类的析构函数。派生类的析构函数(不论你是否显式地定义了)自动调用基类子对象的析构函数。基类在成员对象之后被析构。在多重继承的情况下,直接基类以出现在继承列表中的顺序的反序被析构。
class Member {
 public:
   ~Member();
   
// ...
 };
 
 class Base {
 public:
   virtual ~Base();     
// 虚析构函数,感兴趣的朋友可以参考一下为什么有时候必须用虚析构函数
   
// ...
 };
 
 class Derived : public Base {
 public:
   ~Derived();
   
// ...
 private:
   Member x_;
 };
 
 Derived::~Derived()
 {
   
// 编译器自动调用 x_.~Member()
   
// 编译器自动调用 Base::~Base()
 }

注意:虚拟继承的顺序相关性是多变的。如果你在一个虚拟继承层次中依赖于其顺序相关性,那你应该去别的地方查一下资料
   
 析构函数的析构顺序简单的概括为一句话:“先构造的后析构,也就是说构造的顺序和析构的顺序反着的”。

比如我们有下面的语句:

void Creat

{

    Class_Name A[10];

//other code…

}
此处我们创建了一个对象数组,则他最后的析构顺序为A[9],A[8]…A[0]

所以,结论是,一般不要自作聪明的去调用析构函数。或者要是你不嫌麻烦的话,析构之前最好先看看堆上的数据是不是已经被释放过了。放出一个演示的例子,大家可以参考一下哈。

class aaa
{
public:
    aaa(){}
    ~aaa(){cout<<"deconstructor"<<endl; }
    void disp(){cout<<"disp"<<endl;}
private:
    char *p;
};
void main()
{
aaa a;
a.~aaa();
a.disp();
}
这样的话,显式两次deconstructor,第一次析构相当于调用一个普通的成员函数,执行函数内语句,显示
第二次析构是编译器隐式的调用,增加了释放栈内存的动作,这个类未申请堆内存,所以对象干净地摧毁了,
显式+对象摧毁;

class aaa
{
public:
    aaa(){p = new char[1024];}
    ~aaa(){cout<<"deconstructor"<<endl; delete []p; p=NULL;}
    void disp(){cout<<"disp"<<endl;}
private:
    char *p;
};
void main()
{
aaa a;
a.~aaa();
a.disp();

这样的话,第一次显式调用析构函数,相当于调用一个普通成员函数,执行函数语句,释放了堆内存,但是并未释放栈内存,对象还存在(但已残缺,存在不安全因素);
第二次调用析构函数,再次释放堆内存(此时报异常),然后释放栈内存,对象销毁