首页 | 编程语言 | 网站建设 | 游戏天堂 | 冲浪宝典 | 网络安全 | 操作系统 | 软件时空 | 硬件指南 | 病毒相关 | IT 认证
软讯网络 > 编程语言 > C/C++ > More Effective C++ 条款9:异常
【标  题】:More Effective C++ 条款9:异常
【关键字】:More,Effective,C++
【来  源】:http://blog.csdn.net/wang_junjie/archive/2006/09/07/1189362.aspx

More Effective C++ 条款9:异常

C++新增的异常(exception)机制改变了某些事情,这种改变是深刻的,彻底的,可能
是令人不舒服的。例如使用未经处理的或原始的指针变得很危险。资源泄漏的可能性增
加了。写出具有你希望的行为的构造函数与析构函数变得更加困难。特别小心防止程序
执行时突然崩溃。执行程序和库程序尺寸增加了同时运行速度减少了。
这就使我们所知道的事情。很多使用C++的人都不知道在程序中使用异常,大多数人不知
道如何正确使用它。在异常被抛出后,使软件的行为具有可预测性和可靠性,在众多方
法中至今也没有一个一致的方法能做到这点。(为了深刻了解这个问题,参见Tom Carg
ill写的Exception Handling: A False Sense of Security。有关这些问题的进展情况
的信息,参见Jack Reeves 写的Coping with Exceptions和Herb Sutter写的Exception
-Safe Generic Containers。)
我们知道:程序能够在存在异常的情况下正常运行是因为它们按照要求进行了设计,而
不是因为巧合。异常安全(Exception-safe)的程序不是偶然建立的。一个没有按照要
求进行设计的程序在存在异常的情况下运行正常的概率与一个没有按照多线程要求进行
设计的程序在多线程的环境下运行正常的概率相同,概率为0。
为什么使用异常呢?自从C语言被发明初来,C程序员就满足于使用错误代码(Error co
de),所以为什么还要弄来异常呢,特别是如果异常如我上面所说的那样存在着问题。
答案是简单的:异常不能被忽略。如果一个函数通过设置一个状态变量或返回错误代码
来表示一个异常状态,没有办法保证函数调用者将一定检测变量或测试错误代码。结果
程序会从它遇到的异常状态继续运行,异常没有被捕获,程序立即会终止执行。
C程序员能够仅通过setjmp和longjmp来完成与异常处理相似的功能。但是当longjmp在C
++中使用时,它存在一些缺陷,当它调整堆栈时不能对局部对象调用析构函数。而大多
数C程序员依赖于这些析构函数的调用,所以setjmp和longjmp不能够替换异常处理。如
果你需要一个方法,能够通知异常状态,又不能忽略这个通知,并且搜索栈空间(sear
ching the stack)以便找到异常处理代码时,你还得确保局部对象的析构函数必须被调
用,这时你就需要使用C++的异常处理。
因为我们已经对使用异常处理的程序设计有了很多了解,下面这些条款仅是一个对于写
出异常安全(Exception-safe)软件的不完整的指导。然而它们给任何在C++中使用异常
处理的人介绍了一些重要思想。通过留意下面这些指导,你能够提高自己软件的正确性
,强壮性和高效性,并且你将回避开许多在使用异常处理时经常遇到的问题。
条款9:使用析构函数防止资源泄漏
对指针说再见。必须得承认你永远都不会喜欢使用指针。
Ok,你不用对所有的指针说再见,但是你需要对用来操纵局部资源(local resources)
的指针说再见。假设,你正在为一个小动物收容所编写软件,小动物收容所是一个帮助
小狗小猫寻找主人的组织。每天收容所建立一个文件,包含当天它所管理的收容动物的
资料信息,你的工作是写一个程序读出这些文件然后对每个收容动物进行适当的处理(
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),在
条款25详细描述了这种函数。为了完成我们的目标,我们这样声明函数:
// 从s中读去动物信息, 然后返回一个指针
// 指向新建立的某种类型对象
ALA * readALA(istream& s);
你的程序的关键部分就是这个函数,如下所示:
void processAdoptions(istream& dataSource)
{
while (dataSource) { // 还有数据时,继续循环
ALA *pa = readALA(dataSource); //得到下一个动物
pa->processAdoption(); //处理收容动物
delete pa; //删除readALA返回的对象
}
}
这个函数循环遍历dataSource内的信息,处理它所遇到的每个项目。唯一要记住的一点
是在每次循环结尾处删除ps。这是必须的,因为每次调用readALA都建立一个堆对象。如
果不删除对象,循环将产生资源泄漏。
现在考虑一下,如果pa->processAdoption抛出了一个异常,将会发生什么?processAd
options没有捕获异常,所以异常将传递给processAdoptions的调用者。转递中,proce
ssAdoptions函数中的调用pa->processAdoption语句后的所有语句都被跳过,这就是说
pa没有被删除。结果,任何时候pa->processAdoption抛出一个异常都会导致processAd
options内存泄漏。
堵塞泄漏很容易,
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。替代指针的对象被称为smar
t pointers(灵巧指针),参见条款28的解释,你能使得pointer-like对象非常灵巧。
在这里,我们用不着这么聪明的指针,我们只需要一个pointer-lik对象,当它离开生存
空间时知道删除它指向的对象。
写出这样一个类并不困难,但是我们不需要自己去写。标准C++库函数包含一个类模板,
叫做auto_ptr,这正是我们想要的。每一个auto_ptr类的构造函数里,让一个指针指向
一个堆对象(heap object),并且在它的析构函数里删除这个对象。下面所示的是aut
o_ptr类的一些重要的部分:
template
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和将在条款28讲述的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 pa(readALA(dataSource));
pa->processAdoption();
}
}
这个版本的processAdoptions在两个方面区别于原来的processAdoptions函数。第一,
pa被声明为一个auto_ptr对象,而不是一个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拷贝
//有关一个更灵活的方法的讨论请参见条款28。
WindowHandle(const WindowHandle&);
WindowHandle& operator=(const WindowHandle&);
};
这看上去有些象auto_ptr,只是赋值操作与拷贝构造被显式地禁止(参见条款27),有
一个隐含的转换操作能把WindowHandle转换为WINDOW_HANDLE。这个能力对于使用Windo
wHandle对象非常重要,因为这意味着你能在任何地方象使用raw WINDOW_HANDLE一样来
使用WindowHandle。(参见条款5 ,了解为什么你应该谨慎使用隐式类型转换操作)
通过给出的WindowHandle类,我们能够重写displayInfo函数,如下所示:
// 如果一个异常被抛出,这个函数能避免资源泄漏
void displayInfo(const Information& info)
{
WindowHandle w(createWindow());
在w对应的window中显式信息;
}
即使一个异常在displayInfo内被抛出,被createWindow 建立的window也能被释放。
资源应该被封装在一个对象里,遵循这个规则,你通常就能避免在存在异常环境里发生
资源泄漏。但是如果你正在分配资源时一个异常被抛出,会发生什么情况呢?例如当你
正处于resource-acquiring类的构造函数中。还有如果这样的资源正在被释放时,一个
异常被抛出,又会发生什么情况呢?构造函数和析构函数需要特殊的技术。你能在条款
10和条款11中获取有关的知识。
 
More effective c++ 条款10:在构造函数中防止资源泄漏(上):【上一篇】
Borland全新Turbo C++上手准备:【下一篇】
【相关文章】
  • More effective c++ 条款10:在构造函数中防止资源泄漏(上)
  • More effective c++ 条款10:在构造函数中防止资源泄漏(下)
  • More Effective C++ 条款11:禁止异常信息(exceptions)传递到析构函数外
  • More effective C++ 条款12:理解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”间的差异
  • C++ 题内话两则 ( Copy Constructor & Initialization when object as a field)
  • Java与C++多态性浅析
  • 强大免费的C++跨平台图像处理库 ImageStone V2.0 发布
  • 读书笔记《Programming in C++》之一
  • vc++6.0STL中std::string类导致程序崩溃的解决方案
  • 怎样使用C++回调函数
  • 【随机文章】
  • 如何用DLL最简单地保护您的工程代码
  • 中小企业服务器配置方案 第一章(6)
  • Hacking Diablo II之D2HACKIT技术详解
  • 错误 0xc00470fe: 数据流任务: 产品级别对于 组件“源 - TestDB01$”(1) 而言不足
  • Web服务器(五)为Apache增加SSL安全保护
  • 常用JS网页广告代码
  • AJAX框架汇总
  • 日记 [2006年05月25日]巧妙应用sql
  • 改变PowerPoint超级链接颜色
  • mysqli学习笔记
  • 【相关评论】
    没有相关评论
    【发表评论】
    姓名:
    邮件:
    随机码*
    评论*
          
    |  首 页  |  版权声明  |  联系我们   |  网站地图  |
    CopyRight © 2004-2007 软讯网络 All Rigths Reserved.