漫步云端

移动开发(Android、iPhone、Windows Mobile) | JAVA | C | C++ | .net | Objective C | 微软企业开发技术 | 嵌入式系统设计与开发
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

C++ 与“类”有关的注意事项总结(八):析构函数

Posted on 2011-04-05 23:48  charley_yang  阅读(3700)  评论(0编辑  收藏  举报

一、虚构函数定义

 

      析构函数是一个特殊的由用户定义的成员函数,当该类的对象离开了它的域或者delete表达式应用到一个该类的对象的指针上时,析构函数会自动被调用。 析构函数的名字是在类名前加上波浪线 ~ ,它不返回任何值也没有任何参数,因为它不能指定任何参数, 所以它也不能被重载。尽管我们可以为一个类定义多个构造函数,但是我们只能提供一个析构函数,它将被应用在类的所有对象上。下面是Account 类的析构函数:

 

class Account {
public:
Account();
explicit Account( const char*, double=0.0 );
Account( const Account& );
~Account();
// ...
private:
char *_name;
unsigned int _acct_nmbr;
double _balance;
};
inline
Account::~Account()
{
delete [] _name;
return_acct_nmbr( _acct_nmbr );
}

 

      一般地, 如果一个类的数据成员是按值存储的,比如 Point3d 的三个坐标成员,则无需析构函数。并不是每一个类都要求有析构函数,即使我们为该类定义了一个或多个构造函数 ,析构函数主要被用来放弃在类对象的构造函数或生命期中获得的资源,如释放互斥锁或删除由操作符new分配的内存。

 

      当类对象的指针或引用离开域时,被引用的对象还没有结束生命期, 析构函数不会被调用。 

    C++语言在内部保证,不会用 delete操作符删除不指向任何对象的指针, 所以我们无需再编写代码来保证这一点 :


// 没有必要——由编译器隐式执行
if ( pact != 0 ) delete pact;


      无论何时,当在一个函数内删除一个独立的堆对象时,最好是用 auto_ptr 类对象而不是一个实际的指针 。对于堆上的类对象尤其应该这样做,否则的话,如果应用 delete表达式失败(比如一个异常被抛出的情况下 ),不仅会导致内存泄漏 而且析构函数也不会被调用 。例如, 下面是我们用auto_ptr 重写之后的程序示例,它被稍做修改因为 auto_ptr 对象不支持被显式地重置以指向第二个对象,除非赋值第二个 auto_ptr 。

 

#include <memory>
#include "Account.h"
 
Account global( "James Joyce" );
int main()
{
Account local( "Anna Livia Plurabelle", 10000 );
Account &loc_ref = global;
auto_ptr<Account> pact( new Account( "Stephen Dedalus" ));

{
  Account local_too( “Stephen Hero” );
}
// auto_ptr 对象在此被销毁
}

 

二、显式的析构调用

 

      在某些程序情况下,有必要显示地对一个特殊类对象调用析构函数。这常常发生在和定位 new操作符结合的时候。让我们看一个例子,
当写:


  char *arena = new char [ sizeof Image];


      时,实际上我们已经分配了一个大小等于 Image 型对象的新的堆存储区。相关联的内存区没有被初始化,里面是上次使用之后的一段随机位序列。当我们写:


 Image *ptr = new (arena) Image( “Quasimodo” )


      时,没有新的内存被分配。相反,ptr 被赋值为与 arena 相关联的地址,通过 ptr,内存被解释为一个 Image 类对象。然而,虽然没有分配内存,但是构造函数被应用在现有的存储区上。实际上,定位 new 操作符允许我们在一个特定的、预分配的内存地址上构造一个类对象。

      当完成了 Quasimodo 的图象(image)时,我们或许希望在由 arena 指向的同一个内存位置上操作一个 Esmerelda 的图象(image) 。一方面,我们知道怎样做:


 Image *ptr = new (arena) Image( “Esmerelda” );


      问题是,这样做覆盖了 Quasimodo 的图像,我们已经修改了 Quasimodo 的图像并希望把它存储在磁盘上。一般我们通过 Image 类的析构函数来做到这一点,但是如果应用操作符delete :


//不好:调用析构函数的同时也删除了存储区
delete ptr;

      则除了调用析构函数,我们还删除了底层的堆存储区,这不是我们希望的。我们可以显式地调用 Image 的析构函数:


ptr -> ~ Image();


     底层的存储区可以被后面的定位 new操作符调用继续使用。

 
     尽管 ptr和arena 指向同一个堆存储区没有任何意义,但是,在 arena 上应用 delete 操作符 :


//没有调用析构函数
delete arena;


  不会导致调用 Image 的析构函数,因为 arena 的类型是 char*。记住,只有当 delete 表达式中的指针指向一个带有析构函数的类类型时,编译器才会调用析构函数。
 

三、可能出现的程序代码膨胀

 

     毫无疑问,内联析构函数可能时程序代码膨胀的一个源泉,因为它被插入到函数中的每个退出点,以析构每一个活动的局部类对象。例如,在如下代码段中:


Account acct ( “Tina Lee” );
int swt;

// ...
switch( swt ) {
case 0:
return;
case 1:
// 进行操作
return;
case 2:
// 进行其他操作
return;
// 等等
}

     在每个 return语句之前,析构函数都必须被内联地展开,在 Account 类的析构函数的情况下,由于它的长度较小,所以多次展开的相关开销也较小。但是 如果已经发现它确实是一个问题,则解决方案是:或者把析构函数声明为非内联的,或者重新改写程序代码。一种可能的重写方案是在每个case 标签中用 break语句代替return语句,然后在 switch 语句后面引入一 个 return语句:

 
// 重写来提供一个返回点
switch( swt ) {
case 0:
break;
case 1:
// 进行操作
break;
case 2:
// 进行另一些操作
break;
// 等等
}
 
// 单个返回点
return;