1、异常是一个任意类型的临时变量,用于警示错误。
异常可以是基本类型(int double),但它通常是专门为处理错误而定义的"类对象".
异常对象可以把错误发生的信息传送给处理错误的代码,而且涉及到的信息不只一项.
2、throw 抛出异常 一般配合try来使用.
3、try....catch....用于捕捉异常
try块用于可能出现异常的代码,catch用于发生异常后,对异常的捕捉和处理.
catch仅在try产生异常时才执行,.如果没有发生异常,将跳过这个处理 .
try中只要一生成异常, try块异常后的代码交不会执行,直接去catch块去匹配异常.
4..异常处理 的过程
一.使用throw表达式将临时对象temp初始化为ExceptionTypetemp(theException);
二.为在try块中声明的所有自动对象调用析构函数.(因为跳出try块,异常就不起作用了,临时变量就起到传递异常的作用)
三.选择参数类型与异常类型的"第一个匹配"的处理 程序.
四.使用异常的副本把参数初始化为typeN ex(temp), 并把控制权传递给处理 程序.
五. 除非处理程序中的代码决定结束程序,否则在处理 程序执行完成后,将跳到整个try块后面(即最后一个catch块的右花括号后)相接代码.
5. 处理中注意的问题:
一.一旦抛出异常,会立即退出try块, 那么在try块中声明的所有自动对象将释放.包括异常本身.
二.异常对象必须是可以复制的.如果异常对象的类拷贝构造函数是私有的(禁止复制),那这个对象就用作异常.
因为离开try时会析构异常对象,这之前会将异常传递给临时变量,再由临时变量去匹配catch,这都会涉及拷贝构造函数.
所以,throw表示式初始化临时时就创建了一个异常的一个副本,这样就可以离开try块,接着使用抛出的副本来初始化catch块的参数,并执行catch块中的处理程序.
三.. catch块也是一个作用域. 执行完catch块,其所有本地自动对象(包括参数)一样要释放.
因此上面的控制权是由try中抛出后,控制权交catch,即使catch代码为空,一样执行完后,再交外层.
6. 重点: 未处理的异常
有两种情况: 一是没有设置捕捉程序产生异常;; 二是虽然设置和捕捉程序(try catch),但漏掉了没捉到. 这都会出现"未处理的异常"
未处理的异常按下面流程执行:
调用标准库函数terminate() (它在<exception>头文件中), 这个函数它会调用预先定义的默认中断处理函数,这个默认的中断处理函数再调用abort() (在头
文件<cstdlib>中).
所以流程就是: terminate()---->默认的中断处理程序--------->abort()
这个处理是强制执行,同时因为abort会立即中断,和exit()不同的是,它不会为静态建立的对象调用析构函数(更不用动态的了),这明显是一个不妥的处理.
要改善上面的"强硬"中断,我们须要人为的干预,用我们自己"退出函数"来代替上面的"默认的中断处理函数",这样就不会去调用abort()即:
terminate()------>我们的退出函数
这样就避免了abor()的出现.
如何实现?
标准库函数set_terminate(参数),它接收terminate_handler类型的参数,返回该类型的值.(在头文件<exception>中)
这个类型在exception标准头文件中定义是: typedef void(*terminate_handler)();
注意这个terminate_handler是一个函数指针,没有参数也没返回值,因此,我们定义这个"替代"的"退出函数"也应是这样情况.如:
void myHandler(){
//自己的处理代码
}
看下面的,抛出int型,但捕捉的是double,因此捕捉漏了..进入默认的处理 环节,但由于我们定义了自己的"退出函数",于是进入我们预定义的函数处理
#include <iostream> #include<exception> using namespace std; void fun(){ cout<<"yes"<<endl; exit(-1); } int main(int argc, char *argv[]){ int i=3; terminate_handler p=set_terminate(fun); try{ throw i; } catch(double& e){ cout<<"OK"<<endl; } return 0; }
yes
请按任意键继续. . .
上面的返回值p是一个指针,它指向所设置的旧处理程序(这样存储起来以便在后面需要时恢复它),这里的旧处理程序就是原来的"默认的中断处理函数",
我们只要在这个语句后面再加一个set_terminate(p); 这样就恢复了原来的处理函数,即调用默认的中断处理函数,它会去调用abort(),所以这时结果就是:
This application has requested the Runtime to terminate it in an unusual way.
Please contact the application's support team for more information.
请按任意键继续. . .
注意:第一次调用set_terminate(),返回值是指向默认的处理程序的指针.
以后每次再调用都会建立一个新的处理程序,并返回实际起作用的处理程序的指针.
#include <iostream> #include<exception> using namespace std; void fun(){ cout<<"yes"<<endl; exit(-1); } void fun1(){ cout<<"yes1111"<<endl; exit(-1); } void fun2(){ cout<<"yes2222"<<endl; exit(-1); } int main(int argc, char *argv[]){ int i=3; terminate_handler p=set_terminate(fun); p=set_terminate(fun1); p=set_terminate(fun2);//此时只有这个起作用 // set_terminate(p); try{ throw i; } catch(double& e){ cout<<"OK"<<endl; } return 0; }
yes2222
请按任意键继续. . .
分析:先是用fun去替换了默认的中断处理程序,然后再用fun1()去代替了fun()处理程序,最后用fun2()替换了fun1()处理程序.
因此最后的替换程序是fun2(),所以输出的是yes2222,,注意这里的 p是指向的最近的"被替换的"的fun1()
如果我们去掉注释句,执行set_terminate(p),实际上就是让fun1()去替换.因此此时结果为:
yes1111
请按任意键继续. . .
所以上面的"恢复"原来的处理函数,就是上面的意思.
从上面可以看到,这实际上就有点类似windows编程中的hook技术.
7. try捕捉的强大性
try中出现的异常,哪怕是其中的函数的异常,还是函数中再调用函数的异常,都会被try捕捉.
另外,可以进行预判,如下面,虽然是同样的函数捕捉异常,但在catch块中可以不一样,因为如果可以人为事先料定在哪种情况出问题,就可以
连续写出这样的语句
try{ ... fun();//这块可以预判某处异常 ... } catch(...){ ... } ... try{ ... fun();//这块可以预判另一处异常 ... } catch(...){ ... }
8. 嵌套的try块
原则:外层的try的异常不能被内层捕捉,但是,内层中的异常如果漏了会被外层捕捉.
try{//外层 ... try{//内层 ... } catch(...){//内层,捕获内层异常 ... } ... } catch(...){//外层,捕获外层异常和内层没捕获到的异常 ... }
9. 用类对象作为异常
前提: 定义的异常类的成员函数不能抛出异常.
抛出异常对象的顺序:
先复制异常对象(创建一个临时对象),再释放原对象,因为退出try,该对象就超出了作用域(就要析构)
之后,把对象副本传递给catch处理程序,如果参数是一个引用,就按引用传递.
10. 匹配的处理.
try中抛出的异常类型要与catch中的异常类型匹配,如果一个都没匹配到,就会调用默认的中断处理程序.
如果是基本类型,就要求参数与异常类型完全匹配;
如果是类对象的异常,就会进行自动类型转换,以便匹配处理程序中的参数.
匹配的情况如下:
一.参数类型与异常类型完全匹配,忽略const;
二. 参数类型是异常类类型的直接或间接基类,或是异常类的直接或间接基类的引用. 忽略const;
三.异常与参数都是指针,异常类型可以自动转换为参数类型,忽略const
因此,如果基类参数排前派生类型之前,则总是选择基类参数的处理程序来处理派生的,即:派生类型的处理程序永远不会执行.
11、virtual对动态类型的保留。
原则:一般catch中采用的是参数的引用,一是可以避免拷贝构造,二是加快速度。
当类层次中有virtual时,引用将保留原基类或派生类的动态特性,即:其原类类型不变。如果是按值传送,就不一样了,无论如何都是
要复制,最终的目的是基类,即对象切片现象。
检查一个变量用 typeid() 它返回是type_info类型,可以用typeid().name提取这个类型名。
它头文件<typeinfo>中。
12、重新抛出异常:相当于二传手。
形式: throw; //不跟参数,表明原异常将“原封不动”地再次向外抛出。
注意下面的区别:
#include <iostream> using namespace std; class A{ const char* p; public: A(const char* m="A"):p(m){} A(const A&){cout<<"copy"<<endl;} virtual const char* what()const{return p;} }; class B:public A{ public: B(const char* m="B"):A(m){} }; int main(int argc, char *argv[]){ A a; B b; try{ try{ throw b; } catch(A& e){ throw;//throw e; cout<<"内层"<<endl; } } catch(A& e){ cout<<"外层"<<endl; } return 0; }
copy
外层
请按任意键继续. . .
如果把throw换成throw e;则结果为:
copy
copy
外层
请按任意键继续. . .
这是什么原因呢?
首先抛出throw b;将产生一个临时变量用拷贝构造,b本身析构.这是第一个copy产生的原因.
接着就有区别了:
如果是throw; 则相当于是二传手,因为是引用,直接把这个临时再次抛出(因此不存在着复制问题)
如果是throw e;就不一样了,,它会把再次造就一个临时变量(拷贝构造,第二个copy产生),原来的第一个临时变量因离开本作用区而析构.
第二个临时变量将进入外层中捕捉.
13、捕获所有异常:catch-all
当我们不能明确知道会抛出什么异常时,可以用“捕获所有异常”来进行拦截。
定义: catch(...) //注意,参数用三个点,表示该块处理所有异常。
catch(...){
//code to handle any exception....
}
它适用于我们对异常一无所知,为了防止漏捕,可以这个“终极”大法进行捕捉。
14、抛出异常的函数。
当调用函数语句位于try块时,可以捕获所有函数产生的异常。
但这样有缺点: 它会立即中断函数,造成一些损失。
为了弥补这个bug,可以在函数中(函数体内)或函数体中(整个函数体)进行捕捉相关异常。注意 这与调用函数的代码是有区别的。
定义:只须在函数体左花括号中加上关键字try,再在右花括号后加上catch处理语句。
void fun(int arg)//void fun(int arg) throw(A) try{ //函数代码 } catch(A& a){ //异常a处理 } catch(B& b){ //异常b处理 }
这样就可以处理 比较方便。
一、限制函数可能抛出的异常。
有时为了使得一些异常在函数体中进行处理,而另一些则通过函数外层处理 ,这时就需要把相关异常抛到处层去。
定义:在函数头的后面指明可以抛到外层去的异常 (见上面函数头后面注释句)
(1)函数头后有 throw(A,B)等,说明有限制,设置了一个哨卡,只有A、B类具有通信证可以抛出到外层。
(2)函数头后有throw(), 说明函数有限制,设置了一个哨卡,但没有一个类型可以抛出外层去,即禁止抛出到外层。
(3)函数后没有什么,说明函数没有限制,连哨卡都没有,只要在函数体中漏出的异常都可以抛出到外层去。
注意 :如果函数有异常说明,就必须在函数声明和函数的定义中列出它。
void fun(int arg) throw(A,B);//声明
.....
void fun(int arg) throw(A,B){ ...} //定义
但是在用typedef定义函数指针的类型时,不能包含限制异常说明,因为它不是类型的一部分。
typedef void (*fun)(int); //没有限制说明
fun pfun throw(A,B);
#include <iostream> using namespace std; void fun() throw(int,double)//限制函数体内不能捕获int,double,只有用外层调用函数的try来捕获 try{ int i=1; double j=3.2; char k='e'; throw i; } catch(char& e){ cout<<"char"<<endl; } int main(int argc, char *argv[]){ fun(); return 0; }
------------------------------------
当改为throw k时:结果如下:
char
请按任意键继续. . .
二、未预料到的异常
根据一,我们限制了相关能抛出到外层的异常,但这又引起一个问题。
如果函数体中的有4种异常,函数体能捕捉到1种,能抛出到外层的异常有2种,那么就有一种没法捕获。
这就是未预料的异常。这时会发生什么呢?
标准函数unexpected()----->默认处理函数------>terminate()---->默认中断处理函数----->abort()
如上流程:首先它会调用标准库函数unexpected(),这会使程序暂停,同时,unexpected()调用一个处理函数,这个处理函数的默认操作
就是调用terminate(),terminate()再调用默认的中断处理函数,这个中断处理函数再调用abort()进行强行中断程序。
为了避免它调用默认的unexpected()函数(引发后面的强行中断),可以用一个类似替换terminate()一样的函数来处理 。
做法:把我们自己 的处理函数地址作为参数,调用set_unexpected()函数,这样,就把控制转接到我们自己的函数来。
set_unexpected()的参数类型是:typedef void (*unexpected_handler)( );
因此,同样我们自己的处理函数是不带参数,也没有返回值的。
对于我们定义的处理 函数,必须有三种结束方式:
(1)抛出一个异常,该异常遵循抛出未预料到异常的异常声明。(也就是再次发生这样的异常)
(2)抛出bad_exception类型异常,即标准<exception>中的标准异常类型。
(3)调用terminate()、exit()、abort(),,进行结束。
std::bad_exception类型的作用:可以为这种类型异常提供一个处理 程序,我的处理 程序不行,也许别人还要用到,这样为下一个处理程序
打好基础。
注意: 这个类型与不可预料的类型不一样,bad_exception一旦没有替代的处理 程序,它一定是调用terminate(),而不可预料的类型调用的则
是unexpected().
15、构造函数中的异常:
构造时发生异常,将立即中断程序,不会调用析构函数,特别是那些new的内存是不会释放的.
发生构造时异常很多,比如new不成功(bad_alloc异常),初始化值不满足要求等等。
Example::Example(int count) throw(bad_alloc) try:BassClass(count){ //code } catch(...){ //code }
同函数异常一样定义,但它可以包含初始化列表(如上面,构造基类),这样连基类也捕捉了。
16、析构函数中的异常。
一般的原则是:析构函数是不应该抛出异常的。
为什么呢?因为析构函数一般在抛出异常时提前已经执行了。当我们跳出析构时,会脱离析构函数作用域,这样会再次去调用析构,
就会出错,严重的情况是一直不断地调用析构,这是一个无止的循环。
但是: 有时的确析构函数中有异常,怎么办呢?
应把try....catch...全部放在析构函数中。
重要:uncaught_exception()函数,返回值1或0
即当抛出异常,去执行catch块(哪怕没有代码),则返回0,否则是1.
简单地说,没有处理 异常为1,处理异常为0
这样就可以判断是否处理了这个异常。
#include<exception> #include<iostream> #include<string> class Test{ public: Test(std::string msg):m_msg(msg){ std::cout<<"In Test::Test(\" "<<m_msg<<"\") "<<std::endl; } ~Test(){ std::cout<<"In Test::~Test(\" "<<m_msg<<"\") "<<std::endl <<" std::uncaught_exception( ) = " <<std::uncaught_exception() <<std::endl; } private: std::string m_msg; }; int main(){ Test t1("outside try block "); try{ Test t2("inside try block "); throw 1; } catch(int i){ } }
上面t2,因throw 1中断,自动释放try块,因没有在catch中,所以,为1
当进入catch块,说明被捕捉到了,uncaught_exception()就会置0,当它析构时就会调用显示。