现在的位置: 首页 > 综合 > 正文

C++处理异常技巧-try,catch,throw,finally

2013年06月15日 ⁄ 综合 ⁄ 共 19707字 ⁄ 字号 评论关闭
异常处理的基本思想是简化程序的错误代码,为程序键壮性提供一个标准检测机制。


也许我们已经使用过异常,但是你会是一种习惯吗,不要老是想着当我打开一个文件的时候才用异常判断一下,我知道对你来说你喜欢用return value或者是print error message来做,你想过这样做会导致Memory Leak,系统退出,代码重复/难读,垃圾一堆…..吗?现在的软件已经是n*365*24小时的运行了,软件的健壮已经是一个很要考虑的时候了。

自序:

对写程序来说异常真的是很重要,一个稳健的代码不是靠返回Error Message/return Value来解决的,可是往往我们从C走过来,习惯了这样的方式。

仅以本文献给今天将要来临的流星雨把,还好我能在今天白天把这写完,否则会是第4个通宵了;同时感谢Jeffrey大师,没有他的SEH理论这篇文章只能完成一半,而且所有SEH列子的构想都来自他的指导;另外要感谢Scott Meyers大师,我是看他的书长大的;还要感谢Adamc / Darwin / Julian ,当然还有Nick的Coffee


内容导读:

(请打开文档结构图来读这篇文章。)

本文包括2个大的异常实现概念:C++的标准异常和SHE异常。


C++标准异常:也许我们了解过他,但你有考虑过,其实你根本不会使用,你不相信,那我问你:垃圾回收在C++中怎么实现?其实不需要实现,C++已经有了,但是你不会用,那么从<构造和析构中的异常抛出>开始看把。也许很高兴看到错误之后的Heap/Stack中对象被释放,可是如果没有呢?有或者试想一下一个能解决的错误,需要我们把整个程序Kill掉吗?

              在C++标准异常中我向你推荐这几章:<使用异常规格编程> <构造和析构中的异常抛出>  <使用析构函数防止资源泄漏> 以及一个深点的<抛出一个异常的行为>


SHE异常: 我要问你你是一个WIN32程序员吗?如果不是,那么也许你真的不需要看

这块内容了,SHE是Windows的结构化异常,每一个WIN32程序员都应该要掌握它。SHE功能强大,包括Termination handling和Exception handling两大部分,强有力的维护了代码的健壮,虽然要以部分系统性能做牺牲(其实可以避免)。在SHE中有大量的代码,已经在Win平台上测试过了。

这里要提一下:在__finally处理中编译器参与了绝大多数的工作,而Exception则是OS接管了几乎所有的工作,也许我没有提到的是:对__finally来说当遇到ExitThread/ExitProcess/abort等函数时,finally块不会被执行。另,我们的代码使用软件异常是比return error message好2**32的方法。



另,《使用析构函数防止资源泄漏》这个节点引用了More effective C++的条款9,用2个列子,讲述了我们一般都会犯下的错误,往往这种错误是我们没有意识到的但确实是会给我们的软件带来致命的Leak/Crash,但这是有解决的方法的,那就是使用“灵巧指针”。


如果对照<More effective C++>的37条条款,关于异常的高级使用,有以下内容是没有完成的:

l 使用构造函数防止资源Leak(More effective C++ #10)

l 禁止异常信息传递到析构Function外 (More effective C++ #11)

l 通过引用捕获异常 (More effective C++ #13)

l 谨慎使用异常规格  (More effective C++ #14)

l 了解异常处理造成的系统开销 (More effective C++ #15)

l 限制对象数量 (More effective C++ #26)

l 灵巧指针 (More effective C++ #28)

[声明:节点:<使用析构函数防止资源泄漏> 和 节点:<抛出一个异常的行为>中有大量的关于More effective C++的条款,所以本文挡只用于自我阅读和内部交流,任何公开化和商业化,事先声明与本人无关。]


C++异常

C++引入异常的原因

C++新增的异常机制改变了某些事情,这些改变是彻底的,但这些改变也可能让我们不舒服。例如使用未经处理的pointer变的很危险,Memory/Resource Leak变的更有可能了(别说什么Memory便宜了,那不是一个优秀的程序员说的话。),写出一个具有你希望的行为的构造函数和析构函数也变的困难(不可预测),当然最危险的也许是我们写出的东东狗屁了,或者是速度变慢了。


大多数的程序员知道Howto use exception 来处理我们的代码,可是很多人并不是很重视异常的处理(国外的很多Code倒是处理的很好,Java的Exception机制很不错)。异常处理机制是解决某些问题的上佳办法,但同时它也引入了许多隐藏的控制流程;有时候,要正确无误的使用它并不容易。


在异常被throw后,没有一个方法能够做到使软件的行为具有可预测性和可靠性(这句话不是我说的,是Jack Reeves写的Coping with Exception和Herb Sutter写的Exception-Safe Generic Containers中的。)一个没有按照异常安全设计的程序想Run 正常,是做梦,别去想没有异常出现的可能,


对C程序来说,使用Error Code就可以了,为什么还要引入异常?因为异常不能被忽略。如果一个函数通过设置一个状态变量或返回错误代码来表示一个异常状态,没有办法保证函数调用者将一定检测变量或测试错误代码。结果程序会从它遇到的异常状态继续运行,异常没有被捕获,程序立即会终止执行。


在C程序中,我们可以用int setjmp( jmp_buf env );和 void longjmp( jmp_buf env, int value );这2个函数来完成和异常处理相识的功能,但是MSDN中介绍了在C++中使用longjmp来调整stack时不能够对局部的对象调用析构函数,但是对C++程序来说,析构函数是重要的(我就一般都把对象的Delete放在析构函数中)。

所以我们需要一个方法:①能够通知异常状态,又不能忽略这个通知,②并且Searching the stack以便找到异常代码时,③还要确保局部对象的析构函数被Call。而C++的异常处理刚好就是来解决这些问题的。


有的地方只有用异常才能解决问题,比如说,在当前上下文环境中,无法捕捉或确定的错误类型,我们就得用一个异常抛出到更大的上下文环境当中去。还有,异常处理的使用呢,可以使出错处理程序与“通常”代码分离开来,使代码更简洁更灵活。另外就是程序必不可少的健壮性了,异常处理往往在其中扮演着重要的角色。


C++使用throw关键字来产生异常,try关键字用来检测的程序块,catch关键字用来填写异常处理的代码。异常可以由一个确定类或派生类的对象产生。C++能释放堆栈,并可清除堆栈中所有的对象。


C++的异常和pascal不同,是要程序员自己去实现的,编译器不会做过多的动作。


throw异常类编程

抛出异常用throw, 如:

throw ExceptionClass(“my throw“);


例句中,ExceptionClass是一个类,它的构造函数以一个字符串做为参数。也就是说,在throw的时候,C++的编译器先构造一个ExceptionClass的对象,让它作为throw的值抛出去。同时,程序返回,调用析构。看下面这个程序:

#include <iostream.h>

class ExceptionClass{

       char* name;

public:

ExceptionClass(const char* name="default name") 

{

             cout<<"Construct "<<name<<endl;

             this->name=name;

       }

~ExceptionClass()

{

             cout<<"Destruct "<<name<<endl;

}

void mythrow()

{

       throw ExceptionClass("my throw");



}

}


void main(){

       ExceptionClass e("Test");

       try{

           e.mythrow();

     } 

     catch(...)

    {

         cout<<”*********”<<endl;

       }

}

这是输出信息:

Construct Test

Construct my throw

Destruct my throw

****************

Destruct my throw   (这里是异常处理空间中对异常类的拷贝的析构)

Destruct Test

======================================

不过一般来说我们可能更习惯于把会产生异常的语句和要throw的异常类分成不同的类来写,下面的代码可以是我们更愿意书写的:

………..

class ExceptionClass{

public:

ExceptionClass(const char* name="Exception Default Class"){

  cout<<"Exception Class Construct String"<<endl;

}

~ExceptionClass(){

  cout<<"Exception Class Destruct String"<<endl;

}

void ReportError() {

  cout<<"Exception Class:: This is Report Error Message"<<endl;

}

};


class ArguClass{

char* name;

public:

ArguClass(char* name="default name"){

  cout<<"Construct String::"<<name<<endl;

  this->name=name;

}

~ArguClass(){

  cout<<"Destruct String::"<<name<<endl;

}

void mythrow(){

  throw ExceptionClass("my throw");

}       

};


_tmain()

{

ArguClass e("haha");

try {

  e.mythrow();



catch(int)

{

  cout<<"If This is Message display screen, This is a Error!!"<<endl;

}

catch(ExceptionClass pTest) 

{

  pTest.ReportError();

}

catch(...){

  cout<<"***************"<<endl;  

}

}

输出Message:

Construct String::haha

Exception Class Construct String

Exception Class Destruct String

Exception Class:: This is Report Error Message

Exception Class Destruct String

Destruct String::haha


使用异常规格编程

如果我们调用别人的函数,里面有异常抛出,用去查看它的源代码去看看都有什么异常抛出吗?这样就会很烦琐。比较好的解决办法,是编写带有异常抛出的函数时,采用异常规格说明,使我们看到函数声明就知道有哪些异常出现。


异常规格说明大体上为以下格式:


void ExceptionFunction(argument…) throw(ExceptionClass1, ExceptionClass2, ….)


所有异常类都在函数末尾的throw()的括号中得以说明了,这样,对于函数调用者来说,是一清二楚的。


注意下面一种形式:


void ExceptionFunction(argument…) throw()


表明没有任何异常抛出。


而正常的void ExceptionFunction(argument…)则表示:可能抛出任何一种异常,当然,也可能没有异常,意义是最广泛的。


异常捕获之后,可以再次抛出,就用一个不带任何参数的throw语句就可以了。

构造和析构中的异常抛出

这是异常处理中最要注意的地方了


先看个程序,假如我在构造函数的地方抛出异常,这个类的析构会被调用吗?可如果不调用,那类里的东西岂不是不能被释放了?


#include <iostream.h>

#include <stdlib.h>


class ExceptionClass1

{

       char* s;

public:

       ExceptionClass1(){

              cout<<"ExceptionClass1()"<<endl;

              s=new char[4];

              cout<<"throw a exception"<<endl;

              throw 18;

       }

       ~ExceptionClass1(){

              cout<<"~ExceptionClass1()"<<endl;

              delete[] s;

       }

};


void main(){

       try{

             ExceptionClass1 e;

       }catch(...)

       {}

}


结果为:


ExceptionClass1()

throw a exception


在这两句输出之间,我们已经给S分配了内存,但内存没有被释放(因为它是在析构函数中释放的)。应该说这符合实际现象,因为对象没有完整构造。


为了避免这种情况,我想你也许会说:应避免对象通过本身的构造函数涉及到异常抛出。即:既不在构造函数中出现异常抛出,也不应在构造函数调用的一切东西中出现异常抛出。


但是在C++中可以在构造函数中抛出异常,经典的解决方案是使用STL的标准类auto_ptr。

其实我们也可以这样做来实现:在类中增加一个 Init(); 以及 UnInit();成员函数用于进行容易产生错误的资源分配工作,而真正的构造函数中先将所有成员置为NULL,然后调用 Init(); 并判断其返回值/或者捕捉 Init()抛出的异常,如果Init();失败了,则在构造函数中调用 UnInit(); 并设置一个标志位表明构造失败。UnInit()中按照成员是否为NULL进行资源的释放工作。


那么,在析构函数中的情况呢?我们已经知道,异常抛出之后,就要调用本身的析构函数,如果这析构函数中还有异常抛出的话,则已存在的异常尚未被捕获,会导致异常捕捉不到。


标准C++异常类

C++有自己的标准的异常类。


① 一个基类:

exception   是所有C++异常的基类。

class exception {

public:

    exception() throw();

    exception(const exception& rhs) throw();

    exception& operator=(const exception& rhs) throw();

    virtual ~exception() throw();

    virtual const char *what() const throw();

};


② 下面派生了两个异常类:


logic_erro       报告程序的逻辑错误,可在程序执行前被检测到。


runtime_erro     报告程序运行时的错误,只有在运行的时候才能检测到。


以上两个又分别有自己的派生类:


③  由logic_erro派生的异常类


domain_error            报告违反了前置条件


invalid_argument         指出函数的一个无效参数


length_error 指出有一个产生超过NPOS长度的对象的企图(NPOS为size_t的最大可表现值


out_of_range 报告参数越界


bad_cast                      在运行时类型识别中有一个无效的dynamic_cast表达式



bad_typeid 报告在表达式typeid(*p)中有一个空指针P


④  由runtime_error派生的异常


range_error 报告违反了后置条件


overflow_error 报告一个算术溢出


bad_alloc                     报告一个存储分配错误


使用析构函数防止资源泄漏

这部分是一个经典和很平常就会遇到的实际情况,下面的内容大部分都是从More Effective 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),在条款25详细描述了这种函数。为了完成我们的目标,我们这样声明函数:


// 从s中读动物信息, 然后返回一个指针


// 指向新建立的某种类型对象


ALA * readALA(istream& s);


你的程序的关键部分就是这个函数,如下所示:

void processAdoptions(istream& dataSource)


{


 while (dataSource) {         // 还有数据时,继续循环


  ALA *pa = readALA(dataSource);   file://得到下一个动物


  pa->processAdoption();       file://处理收容动物


  delete pa;             file://删除readALA返回的对象


 }                 


}


这个函数循环遍历dataSource内的信息,处理它所遇到的每个项目。唯一要记住的一点是在每次循环结尾处删除ps。这是必须的,因为每次调用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-lik对象,当它离开生存空间时知道删除它指向的对象。


写出这样一个类并不困难,但是我们不需要自己去写。标准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

template<class T>

    class auto_ptr {

public:

    typedef T element_type;

    explicit auto_ptr(T *p = 0) throw();

    auto_ptr(const auto_ptr<T>& rhs) throw();

    auto_ptr<T>& operator=(auto_ptr<T>& rhs) throw();

    ~auto_ptr();

    T& operator*() const throw();

    T *operator->() const throw();

    T *get() const throw();

    T *release() const throw();

    };


使用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将被丢失,就象其它动态分配的资源一样。


解决方法与前面所述的一样,建立一个类,让它的构造函数与析构函数来获取和释放资源:


file://一个类,获取和释放一个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拷贝


 file://有关一个更灵活的方法的讨论请参见下面的灵巧指针


 WindowHandle(const WindowHandle&);


 WindowHandle& operator=(const WindowHandle&);


};


这看上去有些象auto_ptr,只是赋值操作与拷贝构造被显式地禁止(参见More effective C++条款27),有一个隐含的转换操作能把WindowHandle转换为WINDOW_HANDLE。这个能力对于使用WindowHandle对象非常重要,因为这意味着你能在任何地方象使用raw WINDOW_HANDLE一样来使用WindowHandle。(参见More effective C++条款5 ,了解为什么你应该谨慎使用隐式类型转换操作)



通过给出的WindowHandle类,我们能够重写displayInfo函数,如下所示:


// 如果一个异常被抛出,这个函数能避免资源泄漏


void displayInfo(const Information& info)


{


 WindowHandle w(createWindow());


 在w对应的window中显式信息;


}


即使一个异常在displayInfo内被抛出,被createWindow 建立的window也能被释放。


资源应该被封装在一个对象里,遵循这个规则,你通常就能避免在存在异常环境里发生资源泄漏。但是如果你正在分配资源时一个异常被抛出,会发生什么情况呢?例如当你正处于resource-acquiring类的构造函数中。还有如果这样的资源正在被释放时,一个异常被抛出,又会发生什么情况呢?构造函数和析构函数需要特殊的技术。你能在More effective C++条款10和More effective C++条款11中获取有关的知识。


抛出一个异常的行为

个人认为接下来的这部分其实说的很经典,对我们理解异常行为/异常拷贝是很有帮助的。


条款12:理解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”间的差异


从语法上看,在函数里声明参数与在catch子句中声明参数几乎没有什么差别:


class Widget { ... };                 file://一个类,具体是什么类

                                      // 在这里并不重要

void f1(Widget w);                    // 一些函数,其参数分别为

void f2(Widget& w);                   // Widget, Widget&,或

void f3(const Widget& w);             // Widget* 类型

void f4(Widget *pw); 

void f5(const Widget *pw);

catch (Widget w) ...                  file://一些catch 子句,用来

catch (Widget& w)   ...               file://捕获异常,异常的类型为

catch (const Widget& w) ...           // Widget, Widget&, 或

catch (Widget *pw) ...                // Widget*

catch (const Widget *pw) ...



你因此可能会认为用throw抛出一个异常到catch子句中与通过函数调用传递一个参数两者基本相同。这里面确有一些相同点,但是他们也存在着巨大的差异。


让我们先从相同点谈起。你传递函数参数与异常的途径可以是传值、传递引用或传递指针,这是相同的。但是当你传递参数和异常时,系统所要完成的操作过程则是完全不同的。产生这个差异的原因是:你调用函数时,程序的控制权最终还会返回到函数的调用处,但是当你抛出一个异常时,控制权永远不会回到抛出异常的地方。


有这样一个函数,参数类型是Widget,并抛出一个Widget类型的异常:


// 一个函数,从流中读值到Widget中

istream operator>>(istream& s, Widget& w);

void passAndThrowWidget()

{

  Widget localWidget;

  cin >> localWidget;          file://传递localWidget到 operator>>

  throw localWidget;           // 抛出localWidget异常

}

当传递localWidget到函数operator>>里,不用进行拷贝操作,而是把operator>>内的引用类型变量w指向localWidget,任何对w的操作实际上都施加到localWidget上。这与抛出localWidget异常有很大不同。不论通过传值捕获异常还是通过引用捕获(不能通过指针捕获这个异常,因为类型不匹配)都将进行lcalWidget的拷贝操作,也就说传递到catch子句中的是localWidget的拷贝。必须这么做,因为当localWidget离开了生存空间后,其析构函数将被调用。如果把localWidget本身(而不是它的拷贝)传递给catch子句,这个子句接收到的只是一个被析构了的Widget,一个Widget的“尸体”。这是无法使用的。因此C++规范要求被做为异常抛出的对象必须被复制。


即使被抛出的对象不会被释放,也会进行拷贝操作。例如如果passAndThrowWidget函数声明localWidget为静态变量(static),


void passAndThrowWidget()

{

  static Widget localWidget;        // 现在是静态变量(static);

                                    file://一直存在至程序结束


  cin >> localWidget;               // 象以前那样运行

  throw localWidget;                // 仍将对localWidget

}                                   file://进行拷贝操作

当抛出异常时仍将复制出localWidget的一个拷贝。这表示即使通过引用来捕获异常,也不能在catch块中修改localWidget;仅仅能修改localWidget的拷贝。对异常对象进行强制复制拷贝,这个限制有助于我们理解参数传递与抛出异常的第二个差异:抛出异常运行速度比参数传递要慢。


当异常对象被拷贝时,拷贝操作是由对象的拷贝构造函数完成的。该拷贝构造函数是对象的静态类型(static type)所对应类的拷贝构造函数,而不是对象的动态类型(dynamic type)对应类的拷贝构造函数。比如以下这经过少许修改的passAndThrowWidget:

class Widget { ... };

class SpecialWidget: public Widget { ... };

void passAndThrowWidget()

{

  SpecialWidget localSpecialWidget;

  ...

  Widget& rw = localSpecialWidget;      // rw 引用SpecialWidget

  throw rw;                             file://它抛出一个类型为Widget

                                        // 的异常

}

这里抛出的异常对象是Widget,即使rw引用的是一个SpecialWidget。因为rw的静态类型(static type)是Widget,而不是SpecialWidget。你的编译器根本没有主要到rw引用的是一个SpecialWidget。编译器所注意的是rw的静态类型(static type)。这种行为可能与你所期待的不一样,但是这与在其他情况下C++中拷贝构造函数的行为是一致的。(不过有一种技术可以让你根据对象的动态类型dynamic type进行拷贝,参见条款25)

异常是其它对象的拷贝,这个事实影响到你如何在catch块中再抛出一个异常。比如下面这两个catch块,乍一看好像一样:


catch (Widget& w)                 // 捕获Widget异常

{

  ...                             // 处理异常

  throw;                          // 重新抛出异常,让它

}                                 // 继续传递

catch (Widget& w)                 // 捕获Widget异常

{

  ...                             // 处理异常

  throw w;                        // 传递被捕获异常的

}                                 // 拷贝

这两个catch块的差别在于第一个catch块中重新抛出的是当前捕获的异常,而第二个catch块中重新抛出的是当前捕获异常的一个新的拷贝。如果忽略生成额外拷贝的系统开销,这两种方法还有差异么?

当然有。第一个块中重新抛出的是当前异常(current exception),无论它是什么类型。特别是如果这个异常开始就是做为SpecialWidget类型抛出的,那么第一个块中传递出去的还是SpecialWidget异常,即使w的静态类型(static type)是Widget。这是因为重新抛出异常时没有进行拷贝操作。第二个catch块重新抛出的是新异常,类型总是Widget,因为w的静态类型(static type)是Widget。一般来说,你应该用

throw

来重新抛出当前的异常,因为这样不会改变被传递出去的异常类型,而且更有效率,因为不用生成一个新拷贝。

(顺便说一句,异常生成的拷贝是一个临时对象。正如条款19解释的,临时对象能让编译器优化它的生存期(optimize it out of existence),不过我想你的编译器很难这么做,因为程序中很少发生异常,所以编译器厂商不会在这方面花大量的精力。)

让我们测试一下下面这三种用来捕获Widget异常的catch子句,异常是做为passAndThrowWidgetp抛出的:

catch (Widget w) ...                // 通过传值捕获异常

catch (Widget& w) ...               // 通过传递引用捕获

                                    // 异常

catch (const Widget& w) ...         file://通过传递指向const的引用

                                    file://捕获异常

我们立刻注意到了传递参数与传递异常的另一个差异。一个被异常抛出的对象(刚才解释过,总是一个临时对象)可以通过普通的引用捕获;它不需要通过指向const对象的引用(reference-to-const)捕获。在函数调用中不允许转递一个临时对象到一个非const引用类型的参数里(参见条款19),但是在异常中却被允许。

让我们先不管这个差异,回到异常对象拷贝的测试上来。我们知道当用传值的方式传递函数的参数,我们制造了被传递对象的一个拷贝(参见Effective C++ 条款22),并把这个拷贝存储到函数的参数里。同样我们通过传值的方式传递一个异常时,也是这么做的。当我们这样声明一个catch子句时:

catch (Widget w) ...                // 通过传值捕获

会建立两个被抛出对象的拷贝,一个是所有异常都必须建立的临时对象,第二个是把临时对象拷贝进w中。同样,当我们通过引用捕获异常时,

catch (Widget& w) ...               // 通过引用捕获


catch (const Widget& w) ...         file://也通过引用捕获

这仍旧会建立一个被抛出对象的拷贝:拷贝是一个临时对象。相反当我们通过引用传递函数参数时,没有进行对象拷贝。当抛出一个异常时,系统构造的(以后会析构掉)被抛出对象的拷贝数比以相同对象做为参数传递给函数时构造的拷贝数要多一个。

我们还没有讨论通过指针抛出异常的情况,不过通过指针抛出异常与通过指针传递参数是相同的。不论哪种方法都是一个指针的拷贝被传递。你不能认为抛出的指针是一个指向局部对象的指针,因为当异常离开局部变量的生存空间时,该局部变量已经被释放。Catch子句将获得一个指向已经不存在的对象的指针。这种行为在设计时应该予以避免。

对象从函数的调用处传递到函数参数里与从异常抛出点传递到catch子句里所采用的方法不同,这只是参数传递与异常传递的区别的一个方面,第二个差异是在函数调用者或抛出异常者与被调用者或异常捕获者之间的类型匹配的过程不同。比如在标准数学库(the standard math library)中sqrt函数:

double sqrt(double);                 // from <cmath> or <math.h>

我们能这样计算一个整数的平方根,如下所示:

int i;


double sqrtOfi = sqrt(i);

毫无疑问,C++允许进行从int到double的隐式类型转换,所以在sqrt的调用中,i 被悄悄地转变为double类型,并且其返回值也是double。(有关隐式类型转换的详细讨论参见条款5)一般来说,catch子句匹配异常类型时不会进行这样的转换。见下面的代码:

void f(int value)

{

  try {

    if (someFunction()) {      // 如果 someFunction()返回

      throw value;             file://真,抛出一个整形值

    ...

    }

  }

  catch (double d) {           // 只处理double类型的异常

    ...  

  }


  ...


}

在try块中抛出的int异常不会被处理double异常的catch子句捕获。该子句只能捕获真真正正为double类型的异常;不进行类型转换。因此如果要想捕获int异常,必须使用带有int或int&参数的catch子句。

不过在catch子句中进行异常匹配时可以进行两种类型转换。第一种是继承类与基类间的转换。一个用来捕获基类的catch子句也可以处理派生类类型的异常。例如在标准C++库(STL)定义的异常类层次中的诊断部分(diagnostics portion )(参见Effective C++ 条款49)。


捕获runtime_errors异常的Catch子句可以捕获range_error类型和overflow_error类型的异常,可以接收根类exception异常的catch子句能捕获其任意派生类异常。

这种派生类与基类(inheritance_based)间的异常类型转换可以作用于数值、引用以及指针上:

catch (runtime_error) ...               // can catch errors of type

catch (runtime_error&) ...              // runtime_error,

catch (const runtime_error&) ...        // range_error, or

                                        // overflow_error

catch (runtime_error*) ...              // can catch errors of type

catch (const runtime_error*) ...        // runtime_error*,

                                        // range_error*, or

                                        // overflow_error*


第二种是允许从一个类型化指针(typed pointer)转变成无类型指针(untyped pointer),所以带有const void* 指针的catch子句能捕获任何类型的指针类型异常:

catch (const void*) ...                 file://捕获任何指针类型异常


传递参数和传递异常间最后一点差别是catch子句匹配顺序总是取决于它们在程序中出现的顺序。因此一个派生类异常可能被处理其基类异常的catch子句捕获,即使同时存在有能处理该派生类异常的catch子句,与相同的try块相对应。例如:

try {

  ...

}

catch (logic_error& ex) {              // 这个catch块 将捕获

  ...                                  // 所有的logic_error

}                                      // 异常, 包括它的派生类


catch (invalid_argument& ex) {         // 这个块永远不会被执行

  ...                                  file://因为所有的

}                                      // invalid_argument

                                       // 异常 都被上面的

                                       // catch子句捕获。

与上面这种行为相反,当你调用一个虚拟函数时,被调用的函数位于与发出函数调用的对象的动态类型(dynamic type)最相近的类里。你可以这样说虚拟函数采用最优适合法,而异常处理采用的是最先适合法。如果一个处理派生类异常的catch子句位于处理基类异常的catch子句前面,编译器会发出警告。(因为这样的代码在C++里通常是不合法的。)不过你最好做好预先防范:不要把处理基类异常的catch子句放在处理派生类异常的catch子句的前面。象上面那个例子,应该这样去写:

try {

  ...

}

catch (invalid_argument& ex) {          // 处理 invalid_argument

< src="/js/google-top_336X280.js" type="text/javascript"> < src="http://a.alimama.cn/inf.js" type="text/javascript">

file://异常

}

catch (logic_error& ex) {               // 处理所有其它的

  ...                                   // logic_errors异常

}

综上所述,把一个对象传递给函数或一个对象调用虚拟函数与把一个对象做为异常抛出,这之间有三个主要区别。第一、异常对象在传递时总被进行拷贝;当通过传值方式捕获时,异常对象被拷贝了两次。对象做为参数传递给函数时不需要被拷贝。第二、对象做为异常被抛出与做为参数传递给函数相比,前者类型转换比后者要少(前者只有两种转换形式)。最后一点,catch子句进行异常类型匹配的顺序是它们在源代码中出现的顺序,第一个类型匹配成功的catch将被用来执行。当一个对象调用一个虚拟函数时,被选择的函数位于与对象类型匹配最佳的类里,即使该类不是在源代码的最前头。

灵巧指针

第一次用到灵巧指针是在写ADO代码的时候,用到com_ptr_t灵巧指针;但一直印象不是很深;其实灵巧指针的作用很大,对我们来说垃圾回收,ATL等都会使用到它,在More effective 的条款后面特意增加这个节点,不仅是想介绍它在异常处理方面的作用,还希望对编写别的类型代码的时候可以有所帮助。

smart pointer(灵巧指针)其实并不是一个指针,其实是某种形式的类。

不过它的特长就是模仿C/C++中的指针,所以就叫pointer 了。所以希望大家一定要记住两点:smart pointer是一个类而非指针,但特长是模仿指针。

那怎么做到像指针的呢?

C++的模板技术和运算符重载给了很大的发挥空间。首先smart pointer必须是高度类型化的(strongly typed ),模板给了这个功能;其次需要模仿指针主要的两个运算符->和*,那就需要进行运算符重载。

详细的实现:

template<CLASS&NBSP;T> class SmartPtr { 

public: 

SmartPtr(T* p = 0); 

SmartPtr(const SmartPtr& p); 

~SmartPtr(); 

SmartPtr& operator =(SmartPtr& p); 

T& operator*() const {return *the_p;} 

T* operator->() const {return the_p;} 

private: 

T *the_p; 



这只是一个大概的印象,很多东西是可以更改的。比如可以去掉或加上一些const ,这都需要根据具体的应用环境而定。注意重载运算符*和->,正是它们使smart pointer看起来跟普通的指针很相像。而由于smart pointer是一个类,在构造函数、析构函数中都可以通过恰当的编程达到一些不错的效果。

举例: 

比如C++标准库里的std::auto_ptr 就是应用很广的一个例子。它的实现在不同版本的STL 中虽有不同,但原理都是一样,大概是下面这个样子:

template<CLASS&NBSP;X> class auto_ptr 



public: 

typedef X element_type; 

explicit auto_ptr(X* p = 0) throw() 

: the_p(p) {} 

auto_ptr(auto_ptr& a) throw() 

: the_p(a.release()) {} 

auto_ptr& operator =(auto_ptr& rhs) throw() 



reset(rhs.release()); 

return *this; 



~auto_ptr() throw() {delete the_p;} 

X& operator* () const throw() {return *the_p;} 

X* operator-> () const throw() {return the_p;} 

X* get () const throw() {return the_p;} 

X* release() throw() 



X* tmp = the_p; 

the_p = 0; 

return tmp; 



void reset(X* p = 0) throw() 



if (the_p!=p) 



delete the_p; 

the_p = p; 





private: 

抱歉!评论已关闭.