异常处理
(
2010-6-28
~
29
)
1. 抛出异常
当前程序的代码出现异常时,程序员创建一个包含当前错误信息的对象把出抛出当前语境,把错误信息抛到更大范围的语境里,在更大范围的语境里得到合理的处理措施进行处理,这种方式就是抛出一个异常。
通常会为异常创建特定的类,用
throw(
异常
);
表达式来抛出异常。
throw
会引发以下事情发生:先创建异常对象的一个拷贝(拷贝的来源是哪里来的?),然后给包含
throw(
异常
);
表达式的函数将这个对象返回(返回到哪里?),将异常抛出之前创建的局部对象销毁(这种局部对象的自动清理称为栈反解),退出函数或作用域。
2. 捕获异常
try
块{}
前面提到当抛出一个异常时,会退出函数,但有时不希望退出整个函数,那么就可以用
try
块,把可能会出异常的代码包含到
try
{}
块里面,那么当
try{}
块里面代码抛出异常时就只是退出
try{}
块。
异常处理器(catch(
异常类型){}
子句)
当程序抛出异常时,就会按异常处理器在源代码中的先后顺序寻找(到哪儿寻找?)异常处理器类型与抛出的异常参数相匹配的异常处理器,如果在该层找不到匹配的异常处理器,那么就到上一层语境中找,依次往上直到找到匹配的异常处理器,找到后就当成处理了该异常而不再寻找其它异常处理器,就进入这个异常处理器进行异常处理,异常处理完成就又恢复到正常的程序流程中,如果一直找不到就调用
terminate();
函数退出程序。异常处理类型说明了异常的相关信息:有时也可以不写异常处理类型;也可以在异常类型处写省略号
(…)
来捕获所有异常,也称为全能异常处理器,这样它将不接受任何异常参数,也不知异常信息,它常用来清理资源然后再抛出所捕获的异常。
异常处理对于多态同样有效,一个异常处理器可以处理它自己和它派生类型的异常。
对于在不同地方抛出的相同类型的异常,只写一个异常处理器。
异常处理的两种基本模型:终止和恢复
终止模型是当异常发生后不可能再恢复到异常发生的地方。
恢复模型是当异常发生后,通过异常处理还能回到异常发生的地方并重新执行产生异常的代码以期取得成功,一般是把这个
try
块放在一个
while
循环里,直到达到期望的成功执行为止。但是,这种模型在实践中没有多大用处。
3. 异常匹配
在寻找匹配的异常时,并不要求完全匹配。一个派生类对象或指向派生类对象的引用的异常可以由这个类的基类类型的异常处理器处理。为避免再次拷贝对象,通常在抛出异常时使用引用而不使用对象来捕获异常。
在异常处理器参数通常也使用引用而不使用对象。(为什么)
由于基类类型的处理器可以处理派生类异常,所以一般是把派生类异常处理器放在前面处理派生类异常,把基类异常处理器放在后来处理不太具体的异常。当抛出一个指针时,将会进行标准指针转换来匹配异常,但不会将一种类型的指针转换成另一种类型的指针。
在一个
catch(){}
子句里面可以用一个不带参数的
throw;
表达式重新抛出异常。
4. 不捕获异常
当某个异常没有异常处理器能够捕获时,就会调用
ternimate();
函数来终止程序(在
terminate()
里调用了
abort()
使程序异常终止而退出)。
terminater();
在以下两种情况下会被调用:局部对象析构函数抛出异常时或栈清理时(也就是正在抛出的异常被打断);另一种情况是全局对象或静态对象的构造函数或析构函数抛出异常时。(通常析构函数是不允许抛出异常的)
除了可以调用系统的
set_terminat()
函数,也可以自定义
terminate()
函数。自定义的
terminate()
函数不能有参数,且返回值必须是
void
类型,且在
terminate()
函数里要有程序终止逻辑。
(什么叫原子性?)
5. 清理
异常处理必须确保当程序执行离开一个作用域时,在这个作用域里由构造函数创建的对象的析构函数一定被调用。
如果在一个对象的构造函数里抛出异常,那么这个对象的析构函数就不会被调用。尤其是在构造函数里分配了资源且又在构造函数里发生异常,那么这些资源将不会被释放,这就引起了资源泄露。所以在写构造函数时一定要注意。可以用以下方法来防止资源泄露:
1)在构造函数中捕获异常
,
用于释放资源
2)在构造函数中分配资源,在析构函数中释放资源
采用资源获得式初始化(让每一次资源分配都具有原子性,资源分配成为局部对象生命周期的一部分,当某一资源分配失败时,那么在栈反解时可以恰当清理之前已获得资源的对象)
auto_ptr
类是一个
RAII
类,它封装指向动态分配的内存的指针,可帮助管理这些动态分配的内存,程序自动释放这些内存。
auto_prt
类模板的构造函数接受一个指向类型对象的指针的参数,在类面重载了指针运算符
*
和
->
,使可以象使用原始对象一样使用
auto_ptr
对象。
auto_ptr
可以很容易地用于指针数据成员。由于引用的类对象总是会被析构,所以当对象被析构时,对象的
auto_ptr
成员总能释放它所封装的原始指针。注意:在使用
auto_ptr
模板类时,模板参数不能是指针
,
而是类类型,因为
auto_ptr
已经知道这里是要一个指针。
函数级的
try
块
6. 标准异常
所有的标准异常都派生自
exception
类,而
exception
类主要有两个派生类:
logic_error(
逻辑异常
)
和
runtime_error(
运行时异常
)
。
logic_error
主要检查一些逻辑错误,如传递无效的参数,
runtime_error
主在是在运行时才检查到,如硬件故障或内存耗尽。
7. 异常规格说明
type fun_name(para) throw()
说明函数不会抛出任何异常;
type fun_name(para) throw(excep1,excep2,...)
说明函数会抛出
excep1/excep2/...
异常;
type fun_name(para)
说明不确定函数是否会抛出异常、哪种类型异常。
如果一个函数指明了异常规格说明,而它又抛出了异常规格说明之外的异常,那么就会调用
unexpected()
函数。
在继承情况时,派生类里重写基类的函数时,其异常规格说明要与基类里这个函数的异常规格说明一致,或者少于基类里的,但不能增加其它异常在派生类的异常规格说明表中,否则就会造成依赖于基类该接口的任何程序崩溃。
异常规格说明是主要为非模板类准备的。当无法确定可能产生的异常时,就不用说明异常规格说明。
8. 异常安全
9. 在编程中使用异常
什么时候避免使用异常:
2)
不在异步事件中使用异常