C++中的异常(exception)
副标题#e#
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中抛出的异常,剩下没有捕捉的可以交给上一级函数处理惩罚。
#p#副标题#e#
2.3 throw
throw后头带一个范例的实例,它和catch的干系就象是函数挪用, catch指定形参,throw给出实参。编译器凭据catch呈现的顺序以及catch指定的 参数范例确定一个异常应该由哪个catch来处理惩罚。
throw不必然非要呈现 在try随后的block中,它可以呈此刻任何需要的处所,只要最终有catch可以捕 获它即可。纵然在catch随后的block中,仍然可以继承throw。这时候有两种情 况,一是throw一个新范例的异常,这与普通的throw一样。二是要rethrow当前 这个异常,在这种环境下,throw不带参数即可表达。譬喻:
try{
2.4 函数声明
...
}
catch(int){
throw MyException ("hello exception"); // 抛出一个新的异常
}
catch(float){
throw; // 从头抛出当前的浮 点数异常
}
尚有一个处所与throw要害字有关,就 是函数声明。譬喻:
void foo() throw (int); // 只能抛出int型 异常
void bar() throw (); // 不抛出任何异常
void baz(); // 可以抛出任意范例的异常可能不抛出异常
假如一个函数的声明中带有throw限定符,则在函数体中也必需同样呈现:
void foo() throw (int)
{
...
}
#p#分页标题#e#
这里有一个问题,很是隐蔽,就是纵然你象上面一样编写了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跳过,导致内存泄露,可能其它资源的泄露 。譬喻:
#p#分页标题#e#
void foo()
{
...
MyClass * p = new MyClass();
bar(p);
...
delete p; // 假如bar()中抛出异常,则不会运行到这里!
}
void bar (MyClass * p)
{
throw MyException();
}
对 于这种环境,C++提供了std::auto_ptr这个模板来办理问题。这个常被称为 “智能指针”的模板道理就是,将本来代码中的指针用一个栈上的模 板实例掩护起来,当产生异常unwinding stack的时候,这个模板实例会被析构 ,而在它的析构函数中,指针将被delete,譬喻:
void foo()
{
...
std::auto_ptr<MyClass> p(new MyClass ());
bar(p.get());
...
// delete p; // 这句不再需要了
}
void bar(MyClass * p)
{
throw MyException();
}
岂论bar()是否抛出异常,只要p被析 构,内存就会被释放。
不仅对付内存,对付其他资源的打点也可以参照 这个要领来完成。在ACE[2]中,这种方法被称为Guard,用来对锁举办掩护。
3.5 结构函数和析构函数
结构函数没有返回值,许多处所都推荐通过抛 出异常来通知挪用者结构失败。这是必定是个好的步伐,可是也不很完美。主要 是因为在结构函数中抛出异常并不会激发析构函数的挪用,譬喻:
class foo
{
public:
~foo() {} // 这个函数 将被挪用
};
class bar
{
public:
bar() { c_ = new char[10]; throw -1;}
~bar() { delete c_;} // 这个函 数不会被挪用!
private:
char * c_;
foo f_;
};
void baz()
{
try{
bar b;
}
catch(int){
}
}
在这个例子中,bar 的析构函数不会被挪用,可是尽量如此,foo的析构函数照旧可以被挪用。危险 的是在结构函数中分派空间的c_,因为析构函数没有被挪用而酿成了leak。最好 的办理步伐照旧auto_ptr,利用auto_ptr后,bar类的声明酿成:
class bar
{
public:
bar() { c_.reset(new char[10]); throw -1;}
~bar() { } // 不需要再delete c_了!
private:
auto_ptr<char> c_;
foo f_;
};
析构函数中则不要抛出异常,这一点在Thinking In C++ Volume 2[3]中有明晰表述。假如析构函数中挪用了大概抛出异常的函数,则应该在析构 函数内部catch它。
3.6 什么时候利用异常
到此刻为止,我们已经讨 论完了异常的大部门问题,可以实际操纵操纵了。实际应用中碰着的最让我头疼 的问题就是什么时候应该利用异常,是否应该用异常全面取代“简介 ”中提到的其它错误处理惩罚方法呢?
首先,不能用异常完全取代返回 值,因为返回值的寄义不必然只是乐成或失败,有时候是一个可选择的状态,例 如:
if(customer->status() == active){
...
}
else{
...
}
在这种环境下,岂论返回值是 什么,都是措施可以接管的正常的功效。而异常只能用来表达“异常 ”– 也就是错误的状态。这好象是显而易见的工作,可是实际编程的进程 中有许多越发迷糊其词的时候,碰着这样的环境,首先要思量的就是这个原则。
第二,看看在特定的环境下异常是否会发挥它的利益,而这个利益正好 又不能利用其他技能到达(可能简朴的到达)。好比,假如你正在为电信公司写 一个巨大计费逻辑,那么你虽然但愿在整个计较用度的进程中会合精神去思量业 务逻辑方面的问题,而不是处处需要按照当前返回值判定是否释放前面步调中申 请的资源。这时候利用异常可以让你的代码很是清晰,纵然你有100处申请资源 的处所,只要一个处所会合释放他们就好了。譬喻:
bool bar1 ();
bool bar2();
bool bar3();
bool foo()
{
...
char * p1 = new char[10];
...
if(! bar1()){
delete p1;
return false;
}
...
char * p2 = new char[10];
...
if(!bar2()){
delete p1; // 要释放前面申请 的所有资源
delete p2;
return false;
}
...
char * p3 = new char[10];
...
if(!bar2()){
delete p1; // 要释放前面申请 的所有资源
delete p2;
delete p3;
return false;
}
}
这种流程显然不如:
#p#分页标题#e#
void bar1() throw(int);
void bar2() throw(int);
void bar3() throw(int);
void foo() throw (int)
{
char * p1 = NULL;
char * p2 = NULL;
char * p3 = NULL;
try{
char * p1 = new char[10];
bar1();
char * p2 = new char[10];
bar2 ();
char * p3 = new char[10];
bar3();
}
catch(int){
delete p1; // 会合释放 资源
delete p2;
delete p3;
throw;
}
}
第三,在Thinking In C++ Volume 2[3] 中列了一个什么时候不该该用,什么时候应该用的表,各人可以参考一下。
最后,说一个与异常无关的对象,但也跟措施错误有关的,就是断言 (assert),我在开拓中利用了异常后,很快发明有的人将应该利用assert处理惩罚的 错误界说成了异常。这里稍微提醒一下assert的用法,很是简朴的原则:只有对 于那些可以通过改造措施更正的错误,才可以用assert。返回值、异常显然与其 不在一个层面上,这是C的入门常识。