异常和析构函数

  
C++的异常(exception)机制改变了某些事情,这种改变是深刻的,彻底的。例如:使用未经处理的或原始的指针变得很危险;资源泄漏的可能性增加了;写出具有我们希望的行为的构造函数与析构函数变得更加困难。特别小心防止程序执行时突然崩溃。执行程序和库程序尺寸增加了,同时运行速度降低了。
 
异常被抛出后,使软件的行为具有可预测性和可靠性,在众多方法中至今也没有一个一致的方法能做到这点。
 
程序能够在存在异常的情况下正常运行是因为它们按照要求进行了设计,而不是因为巧合。异常安全(Exception-safe)的程序不是偶然建立的。一个没有按照要求进行设计的程序在存在异常的情况下运行正常的概率与一个没有按照多线程要求进行设计的程序在多线程的环境下运行正常的概率相同,概率为0。
 
为什么使用异常呢?自从C语言被发明初来,C程序员就满足于使用错误代码(Error code),所以为什么还要弄来异常呢,特别是如果异常如上面所说的那样存在着问题。答案是简单的:异常不能被忽略。如果一个函数通过设置一个状态变量或返回错误代码来表示一个异常状态,没有办法保证函数调用者将一定检测变量或测试错误代码。结果程序会从它遇到的异常状态继续运行,异常没有被捕获,程序立即会终止执行。
 
C程序员能够仅通过setjmp和longjmp来完成与异常处理相似的功能。但是当longjmp在C++中使用时,它存在一些缺陷,当它调整堆栈时不能对局部对象调用析构函数。而大多数C++程序员依赖于这些析构函数的调用,所以setjmp和longjmp不能够替换异常处理。如果需要一个方法,能够通知不可被忽略的异常状态,并且搜索栈空间(searching the stack)以便找到异常处理代码时,还得确保局部对象的析构函数必须被调用,这时就需要使用C++的异常处理。
 
 
假设,正在为一个小动物收容所编写软件,小动物收容所是一个帮助小狗小猫寻找主人的组织。每天收容所建立一个文件,包含当天它所管理的收容动物的资料信息,现在的工作是写一个程序读出这些文件然后对每个收容动物进行适当的处理(appropriate processing)。    完成这个程序一个合理的方法是定义一个抽象类,ALA("Adorable Little Animal"),然后为小狗和小猫建立派生类。一个虚拟函数processAdoption分别对各个种类的动物进行处理:
 class ALA {
public:
 virtual void processAdoption() = 0;
 ...
};
class Puppy: public ALA {
public:
 virtual void processAdoption();
 ...
};
class Kitten: public ALA {
public:
 virtual void processAdoption();
 ...
};
 
需要一个函数从文件中读去信息,然后根据文件中的信息产生一个puppy(小狗)对象或者kitten(小猫)对象。这个工作非常适合于虚拟构造器(virtual constructor)。为了完成我们的目标,我们这样声明函数:
// 从s中读去动物信息, 然后返回一个指针指向新建立的某种类型对象
ALA * readALA(istream& s);
程序的关键部分就是这个函数,如下所示:
void processAdoptions(istream& dataSource)
{
 while (dataSource) {                  // 还有数据时,继续循环
    ALA *pa = readALA(dataSource);      //得到下一个动物
    pa->processAdoption();             //处理收容动物
    delete pa;                         //删除readALA返回的对象
 }                                  
}
 
这个函数循环遍历dataSource内的信息,处理它所遇到的每个项目。唯一要记住的一点是在每次循环结尾处删除pa。这是必须的,因为每次调用readALA都建立一个堆对象。如果不删除对象,循环将产生资源泄漏。
现在考虑一下,如果pa->processAdoption抛出了一个异常,将会发生什么?processAdoptions没有捕获异常,所以异常将传递给processAdoptions的调用者。传递中,processAdoptions函数中的调用pa->processAdoption语句后的所有语句都被跳过,这就是说pa没有被删除。结果,任何时候pa->processAdoption抛出一个异常都会导致processAdoptions内存泄漏。
堵塞泄漏很容易 :
void processAdoptions(istream& dataSource)
{
 while (dataSource) {
    ALA *pa = readALA(dataSource);
 try {
      pa->processAdoption();
 }
 catch (...) {              // 捕获所有异常
    delete pa;               // 避免内存泄漏
                             // 当异常抛出时
    throw;                   // 传送异常给调用者
 }
 delete pa;                 // 避免资源泄漏
}                           // 当没有异常抛出时
}
 
但是必须用try和catch对代码进行小改动。更重要的是必须写双份清除代码,一个为正常的运行准备,一个为异常发生时准备。在这种情况下,必须写两个delete代码。象其它重复代码一样,这种代码写起来令人心烦又难于维护,而且它看上去好像存在着问题。不论我们是让processAdoptions正常返回还是抛出异常,都需要删除pa,所以为什么必须要在多个地方编写删除代码呢?
 
我们可以把总被执行的清除代码放入processAdoptions函数内的局部对象的析构函数里,这样可以避免重复书写清除代码。因为当函数返回时局部对象总是被释放,无论函数是如何退出的。(仅有一种例外就是当调用longjmp时。Longjmp的这个缺点是C++率先支持异常处理的主要原因)
 
具体方法是用一个对象代替指针pa,这个对象的行为与指针相似。当pointer-like对象(类指针对象)被释放时,能让它的析构函数调用delete。替代指针的对象被称为smart pointers(灵巧指针),能使得pointer-like对象非常灵巧。在这里,我们用不着这么聪明的指针,我们只需要一个pointer-like对象,当它离开生存空间时知道删除它指向的对象。

写出这样一个类并不困难,但是我们不需要自己去写。标准C++库函数包含一个类模板,叫做auto_ptr,这正是我们想要的。每一个auto_ptr类的构造函数里,让一个指针指向一个堆对象(heap object),并且在它的析构函数里删除这个对象。下面所示的是auto_ptr类的一些重要的部分:
template<class T>
class auto_ptr {
public:
 auto_ptr(T *p = 0): ptr(p) {}        // 保存ptr,指向对象
 ~auto_ptr() { delete ptr; }          // 删除ptr指向的对象
private:
 T *ptr;                              // raw ptr to object
};
 
auto_ptr类的完整代码是非常有趣的,上述简化的代码实现不能在实际中应用。(我们至少必须加上拷贝构造函数,赋值operator和pointer-emulating函数),但是它背后所蕴含的原理应该是清楚的:用auto_ptr对象代替raw指针,将不再为堆对象不能被删除而担心,即使在抛出异常时,对象也能被及时删除。(因为auto_ptr的析构函数使用的是单对象形式的delete,所以auto_ptr不能用于指向对象数组的指针。如果想让auto_ptr类似于一个数组模板,必须自己写一个。在这种情况下,用vector代替array可能更好。)
使用auto_ptr对象代替raw指针,processAdoptions如下所示:
void processAdoptions(istream& dataSource)
{
 while (dataSource) {
    auto_ptr<ALA> pa(readALA(dataSource));
    pa->processAdoption();
 }
}
 
这个版本的processAdoptions在两个方面区别于原来的processAdoptions函数。第一,pa被声明为一个auto_ptr<ALA>对象,而不是一个raw ALA*指针。第二,在循环的结尾没有delete语句。其余部分都一样,因为除了析构的方式,auto_ptr对象的行为就象一个普通的指针。
隐藏在auto_ptr后的思想是:用一个对象存储需要被自动释放的资源,然后依靠对象的析构函数来释放资源,这种思想不只是可以运用在指针上,还能用在其它资源的分配和释放上。想一下这样一个在GUI程序中的函数,它需要建立一个window来显式一些信息:
// 这个函数会发生资源泄漏,如果一个异常抛出
void displayInfo(const Information& info)
{
 WINDOW_HANDLE w(createWindow());
 在w对应的window中显式信息
 destroyWindow(w);
}
 
很多window系统有C-like接口,使用象like createWindow 和destroyWindow函数来获取和释放window资源。如果在w对应的window中显示信息时,一个异常被抛出,w所对应的window将被丢失,就象其它动态分配的资源一样。
 
解决方法与前面所述的一样,建立一个类,让它的构造函数与析构函数来获取和释放资源:
//一个类,获取和释放一个window 句柄
class WindowHandle {
public:
   WindowHandle(WINDOW_HANDLE handle): w(handle) {}
 ~WindowHandle() { destroyWindow(w); }
   operator WINDOW_HANDLE() { return w; }        // see below
private:
 WINDOW_HANDLE w;
 // 下面的函数被声明为私有,防止建立多个WINDOW_HANDLE拷贝
 WindowHandle(const WindowHandle&);
 WindowHandle& operator=(const WindowHandle&);
};
 
这看上去有些象auto_ptr,只是赋值操作与拷贝构造被显式地禁止,有一个隐含的转换操作能把WindowHandle转换为WINDOW_HANDLE。这个能力对于使用WindowHandle对象非常重要,因为这意味着能在任何地方象使用raw WINDOW_HANDLE一样来使用WindowHandle。
通过给出的WindowHandle类,我们能够重写displayInfo函数,如下所示:
// 如果一个异常被抛出,这个函数能避免资源泄漏
void displayInfo(const Information& info)
{
 WindowHandle w(createWindow());
 在w对应的window中显式信息;
}
    即使一个异常在displayInfo内被抛出,被createWindow 建立的window也能被释放。
    资源应该被封装在一个对象里,遵循这个规则,通常就能避免在存在异常环境里发生资源泄漏。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值