(十七)错误和异常处理

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;
}


结果:
This application has requested the Runtime to terminate it in an unusual way.
Please contact the application's support team for more information.
请按任意键继续. . .

------------------------------------

当改为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){ 
	} 
}

   In   Test::Test(" outside   try   block ")
In   Test::Test(" inside   try   block ")
In   Test::~Test(" inside   try   block ")
     std::uncaught_exception(   )   =   1
In   Test::~Test(" outside   try   block ")
     std::uncaught_exception(   )   =   0
请按任意键继续. . .

       上面t2,因throw 1中断,自动释放try块,因没有在catch中,所以,为1

      当进入catch块,说明被捕捉到了,uncaught_exception()就会置0,当它析构时就会调用显示。









你可能感兴趣的:((十七)错误和异常处理)