1.简介
异常是由语言提供的运行时刻错误处理的一种方式。提到错误处理,即使不提到异常,你大概也已经有了丰富的经验,但是为了可以清楚的看到异常的好处,我们还是不妨来回顾一下常用的以及不常用的错误处理方式。
1.1 常用的错误处理方式
返回值。我们常用函数的返回值来标志成功或者失败,甚至是失败的原因。但是这种做法最大的问题是如果调用者不主动检查返回值也是可以被编译器接受的,你也奈何不了他:) 这在C++中还导致另外一个问题,就是重载函数不能只有不同的返回值,而有相同的参数表,因为如果调用者不检查返回值,则编译器会不知道应该调用哪个重载函数。当然这个问题与本文无关,我们暂且放下。只要谨记返回值可能被忽略的情况即可。
全局状态标志。例如系统调用使用的errno。返回值不同的是,全局状态标志可以让函数的接口(返回值、参数表)被充分利用。函数在退出前应该设置这个全局变量的值为成功或者失败(包括原因),而与返回值一样,它隐含的要求调用者要在调用后检查这个标志,这种约束实在是同样软弱。全局变量还导致了另外一个问题,就是多线程不安全:如果多个线程同时为一个全局变量赋值,则调用者在检查这个标志的时候一定会非常迷惑。如果希望线程安全,可以参照errno的解决办法,它是线程安全的。
1.2 不常用的处理方式
setjmp()/longjmp()。可以认为它们是远程的goto语句。根据我的经验,它们好象确实不常被用到,也许是多少破坏了结构化编程风格的原因吧。在C++中,应该是更加的不要用它们,因为致命的弱点是longjmp()虽然会unwinding stack(这个词后面再说),但是不会调用栈中对象的析构函数--够致命吧。对于不同的编译器,可能可以通过加某个编译开关来解决这个问题,但太不通用了,会导致程序很难移植。
1.3 异常
现在我们再来看看异常能解决什么问题。对于返回值和errno遇到的尴尬,对异常来说基本上不存在,如果你不捕获(catch)程序中抛出的异常,默认行为是导致abort()被调用,程序被终止(core dump)。因此你的函数如果抛出了异常,这个函数的调用者或者调用者的调用者,也就是在当前的call stack上,一定要有一个地方捕获这个异常。而对于setjmp()/longjmp()带来的栈上对象不被析构的问题对异常来说也是不存在的。那么它是否破坏了结构化(对于OO paradigms,也许应该说是破坏了流程?)呢?显然不是,有了异常之后你可以放心的只书写正确的逻辑,而将所有的错误处理归结到一个地方,这不是更好么?
综上所述,在C++中大概异常可以全面替代其它的错误处理方式了,可是如果代码中到处充斥着try/throw/catch也不是件好事,欲知异常的使用技巧,请保持耐心继续阅读:)
2. 异常的语法
在这里我们只讨论一些语法相关的问题。
2.1 try
try总是与catch一同出现,伴随一个try语句,至少应该有一个catch()语句。try随后的block是可能抛出异常的地方。
2.2 catch
catch带有一个参数,参数类型以及参数名字都由程序指定,名字可以忽略,如果在catch随后的block中并不打算引用这个异常对象的话。参数类型可以是build-in type,例如int, long, char等,也可以是一个对象,一个对象指针或者引用。如果希望捕获任意类型的异常,可以使用“...”作为catch的参数。
catch不一定要全部捕获try block中抛出的异常,剩下没有捕获的可以交给上一级函数处理。
2.3 throw
throw后面带一个类型的实例,它和catch的关系就象是函数调用,catch指定形参,throw给出实参。编译器按照catch出现的顺序以及catch指定的参数类型确定一个异常应该由哪个catch来处理。
throw不一定非要出现在try随后的block中,它可以出现在任何需要的地方,只要最终有catch可以捕获它即可。即使在catch随后的block中,仍然可以继续throw。这时候有两种情况,一是throw一个新类型的异常,这与普通的throw一样。二是要rethrow当前这个异常,在这种情况下,throw不带参数即可表达。例如:
try{
...
}
catch(int){
throw MyException("hello exception"); // 抛出一个新的异常
}
catch(float){
throw; // 重新抛出当前的浮点数异常
}
2.4 函数声明
还有一个地方与throw关键字有关,就是函数声明。例如:
void foo() throw (int); // 只能抛出int型异常
void bar() throw (); // 不抛出任何异常
void baz(); // 可以抛出任意类型的异常或者不抛出异常
如果一个函数的声明中带有throw限定符,则在函数体中也必须同样出现:
void foo() throw (int)
{
...
}
这里有一个问题,非常隐蔽,就是即使你象上面一样编写了foo()函数,指定它只能抛出int异常,而实际上它还是可能抛出其他类型的异常而不被编译器发现:
void foo() throw (int)
{
throw float; // 错误!异常类型错误!会被编译器指出
...
baz(); // 正确!baz()可能抛出非int异常而编译器又不能发现!
}
void baz()
{
throw float;
}
这种情况的直接后果就是如果baz()抛出了异常,而调用foo()的代码又严格遵守foo()的声明来编写,那么程序将abort()。这曾经让我很恼火,认为这种机制形同虚设,但是还是有些解决的办法,请参照“使用技巧”中相关的问题。
3. 异常使用技巧
3.1 异常是如何工作的
为了可以有把握的使用异常,我们先来看看异常处理是如何工作的。
3.1.1 unwinding stack
我们知道,每次函数调用发生的时候,都会执行保护现场寄存器、参数压栈、为被调用的函数创建堆栈这几个对堆栈的操作,它们都使堆栈增长。每次函数返回则是恢复现场,使堆栈减小。我们把函数返回过程中恢复现场的过程称为unwinding stack。
异常处理中的throw语句产生的效果与函数返回相同,它也引发unwinding stack。如果catch不是在throw的直接上层函数中,那么这个unwinding的过程会一直持续,直到找到合适的catch。如果没有合适的catch,则最后std::unexpected()函数被调用,说明发现了一个没想到的异常,这个函数会调用std::terminate(),这个terminate()调用abort(),程序终止(core dump)。
在“简介”中提到的longjmp()也同样会unwinding stack,但是这是一个C函数,它就象free()不会调用对象的析构函数一样,它也不知道在unwinding stack的过程中调用栈上对象的析构函数。这是它与异常的主要区别。
3.1.2 RTTI
在unwinding stack的过程中,程序会一直试图找到一个“合适”的catch来处理这个异常。前面我们提到throw和catch的关系很象是函数调用和函数原型的关系,多个catch就好象一个函数被重载为可以接受不同的类型。根据这样的猜测,好象找到合适的catch来处理异常与函数重载的过程中找到合适的函数原型是一样的,没有什么大不了的。但实际情况却很困难,因为重载的调用在编译时刻就可以确定,而异常的抛出却不能,考虑下面的代码:
void foo() throw (int)
{
throw int;
}
void bar()
{
try{
foo();
}
catch(int){
...
}
catch(float){
...
}
}
void baz()
{
try{
foo();
}
catch(int){
...
}
catch(float){
...
}
}
foo()在两个地方被调用,这两次异常被不同的catch捕获,所以在为throw产生代码的时候,无法明确的指出要由哪个catch捕获,也就是说,无法在编译时刻确定。
仍然考虑这个例子,让我们来看看既然不能在编译时刻确定throw的去向,那么在运行时刻如何确定。在bar()中,一列catch就象switch语句中的case一样排列,实际上是一系列的判断过程,依次检查当前异常的类型是否满足catch指定的类型,这种动态的,在运行时刻确定类型的技术就是RTTI(Runtime Type Identification/Information)。深度探索C++对象模型[1]中提到,RTTI就是异常处理的副产品。关于RTTI又是一个话题,在这里就不详细讨论了。
3.2 是否继承std::exception?
是的。而且std::exception已经有了一些派生类,如果需要可以直接使用它们,不需要再重复定义了。
3.3 每个函数后面都要写throw()?
尽管前面已经分析了这样做也有漏洞,但是它仍然是一个好习惯,可以让调用者从头文件得到非常明确的信息,而不用翻那些可能与代码不同步的文档。如果你提供一个库,那么在库的入口函数中应该使用catch(...)来捕获所有异常,在catch(...)中捕获的异常应该被转换(rethrow)为throw列表中的某一个异常,这样就可以保证不会产生意外的异常。
3.4 guard模式
异常处理在unwinding stack的时候,会析构所有栈上的对象,但是却不会自动删除堆上的对象,甚至你的代码中虽然写了delete语句,但是却被throw跳过,导致内存泄露,或者其它资源的泄露。例如:
void foo()
{
...
MyClass * p = new MyClass();
bar(p);
...
delete p; // 如果bar()中抛出异常,则不会运行到这里!
}
void bar(MyClass * p)
{
throw MyException();
}