什么是异常呢?
在编程语言里,按照出现错误的时机来区分,有编译期错误和运行期错误之分。
编译期错误大家肯定很熟悉了,当我们build一个程序时,console里出现的那些error提示就是编译期错误。这些错误是在编译期就能被编译器检查出来。
运行期错误大家可能不太经常见,因为自己写的程序往往都是在“温室”里运行的,很少看到程序崩溃的情况。运行期错误的种类很多,举个例子,当我们要在堆上申请内存的时候,内存空间不足,或者在创建文件的时候,磁盘空间不足,这些都是运行期错误。我们用一个名字——异常来称呼这样的运行期错误。
C++如何捕获异常?
C++有自己异常处理体系,它捕获异常的语法与java和C#很相似,可以看下面的代码:
1 int main(){ 2 3 try{ 4 cout<<"This an easy exception example."<<endl; 5 throw 1; 6 }catch(int i){ 7 cout<<"Catched the exception: i = "<<i<<endl; 8 } 9 10 return 0; 11 }
语法很简单,用try block来包含要捕获异常区域的代码,这段代码里会有throw语句抛出异常,再用catch来捕获异常,上面代码的输出结果如下:
This an easy exception example.
Catched the exception: i = 1
请按任意键继续. . .
慎用catch(...)
C++的异常捕获提供了一个万能捕获器,就是catch(...),它可以捕获任意的异常,可以看出来,因为没有参数名,这样我们就没办法获取异常传递过来的内容了。不过还有一个重要的问题,就是catch(...)的位置问题。下面看一段代码:
1 try{ 2 cout<<"This an easy exception example."<<endl; 3 throw 1; 4 }catch(...){ 5 cout<<"Catched the exception."<<endl; 6 }catch(int i){ 7 cout<<"Catched the exception: i = "<<i<<endl; 8 }
上面的代码把catch(...)放到了catch(int i)之前,这样有什么问题呢?catch(int i)包含的异常处理块永远不会被执行。强悍一点的编译器会为我们指出错误,但是有些编译器就没那么强大了。所以,记住一条准则:catch(...)永远放到所有捕获catch处理的最后一个。
慎用继承体系里的类作为catch的参数
这个问题跟上面的问题类似,也是catch的优先级问题,看下面的代码:
1 try{ 2 DerivedClass dc; 3 cout<<"This an easy exception example."<<endl; 4 throw dc; 5 }catch(SuperClass s){ 6 cout<<"Catched the exception:SuperClass."<<endl; 7 }catch(DerivedClass d){ 8 cout<<"Catched the exception:DerivedClass."<<endl; 9 }
上面的代码中,我们抛出了DerivedClass类的对象,本以为会进入catch(DerivedClass d)的处理块的,但是事实上它之调用了catch(SuperClass s)的处理块。这个代码编译器不会去检查,只能靠我们自己把握了,记住一个准则:要把最高级别的父类放到最后一个Catch里处理。
对象析构函数被调用的三种场合?
对象析构函数什么时候会被调用呢?这里先说一下,有三种情况析构函数被调用。哪三种呢?先看我们熟悉的两种:
1 void func(){ 2 A a; 3 }
第一种:上面的函数在调用时,函数完成调用后,会自动调用A的对象a的析构函数。
1 A *a = new A(); 2 delete a;
第二种:显示的调用delete语句也会调用对象的析构函数。
那第三种是什么呢?其实你已经看到了,就是异常处理区域的throw语句,看下面的代码:
1 try{ 2 DerivedClass dc; 3 cout<<"This an easy exception example."<<endl; 4 throw dc; 5 }
上面的代码,throw语句实际上为我们调用了一次析构函数,尽管这个函数后面可能还有语句。实际上在抛出一个对象的时候,异常体系已经复制了一个tem_dc的对象。然后再调用DerivedClass的析构函数。所以,下面的代码让我们感到很恼怒:
1 try{ 2 DerivedClass *pDc = new DerivedClass(); 3 throw pDc; 4 }
我们希望把pDc指向的对象throw出来,实际上我们只是抛出了pDc这个指针,而这个对象早已经被析构掉了。所以这里记住一个准则:尽量不要抛出指针和引用。
不要在异常处理体系中寄希望与类型转换
不要期望异常处理体系为我们完成类型转换,看下面的代码:
1 try{ 2 throw 'a'; 3 }catch(int ch){ 4 cout<<"this is a ch"<<endl; 5 }
平时写代码的时候,'a'是可以转换成asic码值的,但是这个时候就不行了,程序运行期是错误的。所以,记住一个准则:不要寄希望于C++异常处理体系会帮你做类型转换。
有C++异常处理体系捕获不到的东西吗?
有的,但也没有,什么意思呢。本来有的,后来被解决了。不知道你还记不记得住C++的成员初始化列表,不记得的话,咱们看下面的代码:
1 class Test{ 2 private: 3 int age; 4 public: 5 Test():age(initialze(1)){ 6 7 } 8 int initialze(int i){ 9 if(i == 1){ 10 throw 1; 11 }else{ 12 return 1; 13 } 14 } 15 };
上面的代码中,构造函数在成员初始化列表中调用了可能抛出异常的initialize函数,这样的异常怎么捕获呢?用前辈的一句话:C++于是提供了一种很丑的语法来解决这个问题。怎么解决的呢?看下面的代码:
1 Test() 2 try:age(initialze(1)){ 3 { 4 //函数体 5 } 6 }catch(int i){ 7 cout<<"exception"<<endl; 8 }
是不是很丑陋的语法,我也觉得怎么好看,不过确实捕获了成员初始化列表的异常。
set_unexpected函数的用处
这个函数的作用,简而言之,就是设置默认异常处理函数。什么意思呢?看下面的代码就了解了。
1 void myFunc(){ 2 cout<<"set_unexpected Exception."<<endl; 3 throw 0; 4 } 5 void fun(int x) throw(char) 6 { 7 throw 'a'; 8 } 9 int main(){ 10 set_unexpected(myFunc); 11 try{ 12 fun(1); 13 }catch(int i){ 14 cout<<"int exception"<<endl; 15 } 16 17 system("pause"); 18 return 0; 19 }
从main开始看,我们注册了一个默认异常处理函数,这个函数会对异常做一个修正。fun函数里抛出char的异常,我们的语句是捕获不了的,所以经过默认处理函数修正之后,就可以用catch(int i)捕获了。
不过,上面的代码在VS上运行是不行的,linux下运行就okay了。
Effective C++:不要让异常逃离析构函数
看懂了上面的,你就已经很厉害啦,当然这只是个玩笑。EffiectiveC++里有一条:不要让异常逃离析构函数。什么意思呢?就是当我们遇到下面这样的代码后:
1 try{ 2 DerivedClass dc; 3 cout<<"This an easy exception example."<<endl; 4 throw dc; 5 }catch(SuperClass s){ 6 cout<<"Catched the exception:SuperClass."<<endl; 7 }catch(DerivedClass d){ 8 cout<<"Catched the exception:DerivedClass."<<endl; 9 }
上面的代码throw dc之后,会调用DerivedClass的析构函数,这样的话,如果析构函数再抛出异常,我们的捕获函数就悲剧的不知道该如何处理了。也就是说,当同时出现两个throw抛出的异常之后,程序就会直接宕掉。所以,不要让异常逃离析构函数,否则,你就悲剧了。