异常是一种能够处理非正常行为的机制,也是异常产生端和异常处理端通信的一种方式。在软件开发时,通常的情况是,一方知道会发生何种异常但不知道如何处理,另一方不知道是否会发生异常,但在发生异常时它能够进行处理。所以,C++提供了异常处理机制。允许库的开发者在发生异常时抛出异常,而库的用户捕捉异常并进行合理的处理。
C++中与异常处理相关的关键字有:
throw:抛出异常。
try:测试某个程序块是否会抛出异常。
catch:对异常进行捕捉,然后处理。
因此,一般的异常处理程序的框架是:
库的开发者端:
void throw_test() { if(something_is_bad) { throw exception; } }
而库的用户端:
void catch_test() { try { throw_test(); } catch(exception & e) { // exception handler } }
如上所示,在throw_test()中抛出一个exception异常,而在catch_test()中对throw_test()进行测试,如果发生了异常,就会被catch语句捕捉,并进行处理。
1 捕捉端是否要使用引用
读者看到上面捕捉异常时使用的是引用,那么,是否一定要使用应用呢?
一般传递参数的方式有三种:传递指针、传递值、传递应用。其实传递指针和传递值是类似的。
在捕捉异常时,如果采用指针传递,抛出异常后,就会脱离当前作用域,因此,就必须保证指针所指向的对象依然存在,可以有三种方式来保证:static对象、全局对象、堆上的对象。如果采用static和全局变量,这通常是不符合程序员的习惯,因为每次传递一个异常都必须声明一个static和全局变量,这当然不是良好的编程风格。如果采用堆上的对象,在抛出端进行new,那何时进行delete呢?用户必须保证在最后进行处理之后对对象进行delete,这对程序员产生了极大的负担。
在捕捉异常时,如果采用值传递,就必须产生两次拷贝,而且没有多态行为。具体的请看第2部分。
因此,在抛出对象时,应该尽量使用引用传递。采用应用传递没有上面的问题。
2 throw之后究竟发生了什么?
采用引用传递,通常在抛出端会有下面两种形式:
derived d; throw d; throw derived();
第一种方式是用的局部对象,第二中方式是用的临时对象。那么,离开当前作用域时,d会被析构,那么,它是如果保证d的对象能够传递到捕捉端呢?
当throw一个对象时,编译器会在堆上用抛出的对象复制构造一个临时的对象(G++的实现),因此,就算离开了当前作用域,在堆上同样有一个对象是抛出对象的副本。此时,根据捕捉端会有不同的行为:
由于传递指针在异常处理中通常是不合习惯的,因此,这里并不讨论传递指针。
如果捕捉端是以catch by value捕捉异常:
void catch_test() { try { throw_test(); } catch(exception e) { // exception handler } }
就会调用拷贝构造函数对e进行构造。因此,如果以值捕捉异常,通常会有两次拷贝构造函数的调用。
如果捕捉端是以catch by reference捕捉对象:
void catch_test() { try { throw_test(); } catch(exception& e) { // exception handler } }
就会将e绑定到那个堆上的临时对象。因此,引用传递只有一次拷贝构造函数的调用,而且,将一个基类的引用绑定到一个派生类的对象,该引用还可以使用多态的行为。
而如果以值来捕捉异常,抛出的是派生类的对象,捕捉时用的是基类的对象,此时,会发生对象切割,没有多态行为。(跟对象的参数传递类似)
这也验证了第1部分中最好以引用捕捉异常。
未完。。。