所谓异常,顾名思义就是不正常,有问题。
对于人来说有不正常的时候即生病身体不适,那么对于程序也一样,也有不正常即代码“有病”。
那么,既然有病就要治疗,就要对症下药!这样才能恢复正常。
废了这么多话,还是引出我们C++的“异常”概念。
异常,让一个函数可以在发现自己无法处理的错误时抛出一个异常,希望它的调用者可以直接或者间接处理这个问题。
而传统的异常处理方法:
1.终止程序
2.返回一个表示错误的值(很多系统函数都是这样,例如malloc,内存不足,分配失败,返回NULL指针)
3.返回一个合法值,让程序处于某种非法的状态(最坑爹的东西,有些第三方库真会这样)
4.调用一个预先准备好在出现"错误"的情况下用的函数。
第一种情况是不允许的,无条件终止程序的库无法运用到不能当机的程序里。
第二种情况,比较常用,但是有时不合适,例如返回错误码是int,每个调用
都要检查错误值,极不方便,也容易让程序规模加倍(但是要精确控制逻辑,我觉得这种方式不错)。
第三种情况,很容易误导调用者,万一调用者没有去检查全局
变量errno或者通过其他方式检查错误,那是一个灾难,而且这种方式在并发的情况下不能很好工作。
至于第四种情况,本人觉得比较少用,而且回调的代码不该多出现。
使用异常,就把错误和处理分开来,由库函数抛出异常,由调用者捕获这个异常,调用者就可以知道程序函数库调用出现错误了,并去处理,而是否终止程序就把握在调用者手里了。
但是,错误的处理依然是一件很困难的事情,C++的异常机制为程序员提供了一种处理错误的方式,使程序员可以更自然的方式处理错误。
假设我们写一个程序,简单的除法程序:
int Div(int a, int b) { if(b == 0) exit(1);// 若是return 0;呢?(不可取,返回0万一是10/100呢) return a/b; } int main() { int a = 10; int b = 2; // 若 b = 0 呢 ? cout<<Div(a,b)<<endl; return 0; }
这样的程序,乍一看确实是没问题,但是程序在执行中当除数为0时终止了,终止意味着程序将不会继续往下执行,这就是所谓的异常。但是这样直接终止是不是有点简单粗暴呢?? 这一般不是我们想要的结果。
C++异常中的三把斧头:try,throw,catch
①. 测试某段程序会不会发生异常
②. 若有异常发生,则通过throw抛出该异常(注:抛出的是该变量的类型)
③. 捕获相应的异常,即匹配类型的异常,进行针对性的处理
对应代码:
float Div(int a, int b) { if(b == 0) { throw b;//抛出异常 } return a/b; } int main() { int a = 10; int b = 0; float result = 0.0f; try { result = Div(a,b); } catch(int) { cout<<"Div error!,除数为0"<<endl; } cout<<"result = "<<Div(a,b)<<endl; return 0; }
运行出结果:
我们发现,之前没有抛出异常时,程序会崩溃(除强制结束程序外),而现在没有崩溃,并且反映出了问题所在。
实际上,程序中所包含的异常现象在自身不做处理时,会交给操作系统来处理,而操作系统管理整个机器正常运转,遇到这种异常,它会直接一刀切,结束掉程序,所以会发生崩溃。而若是程序自己写了异常处理,则异常的处理由自己处理。也就是说,异常处理机制即是操作系统下发的二级机构,这个二级机构专门针对自己程序所设定的异常进行处理。
而,程序并非一个返回值,我们看下面:
左边正常返回,发生异常从右边返回,发生异常后,throw之后的代码不会再执行,直接找catch惊醒捕获。那么由异常规范:
class Test {}; float Div(int a, int b)throw(int,double,short,Test)
这就是说该函数只能抛出基本类型int,double,short,以及自定义类型Test
float Div(int a, int b)throw()
这个代表该函数不能抛出异常
float Div(int a, int b)
这个代表可能抛出任何异常
此时又有一个捕获时的类型匹配问题:
float Div(int a, int b) { if(b == 0) { short x = 0; throw x;//抛出异常 } return a/b; } int main() { int a = 10; int b = 0; float result = 0.0f; try { result = Div(a,b); } catch(int) { cout<<"Div error!(int),除数为0"<<endl; } catch(short) { cout<<"Div error!(short),除数为0"<<endl; } //如果抛出的是double或者char又或者其他类型呢?难道还要一直增加catch? //按照下面的方式可以对其他类型进行捕获 catch(...) // 捕获除上面的int和short,且只能放在最后! { cout<<"Div error!(all),除数为0"<<endl; } cout<<"result = "<<Div(a,b)<<endl; return 0; }
这有么有很像哦我们之前学习的switch() ; case: 语句呢?
switch() { case: case: . . default: }
相当于说,不能匹配所有的case语句,再执行default。同样,异常中亦是如此。
总结:
异常的抛出和捕获
- 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个处理代码。
- 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
- 抛出异常后会释放局部存储对象,所以被抛出的对象也就还给系统了,throw表达式会初始化一个抛出特殊的异常对象副本(匿名对象),异常对象由编译管理,异常对象在传给对应的catch处理之后撤销。
栈展开
1. 抛出异常的时候,将暂停当前函数的执行,开始查找对应的匹配catch子句。
2. 首先检查throw本身是否在catch块内部,如果是再查找匹配的catch语句。
3. 如果有匹配的,则处理。没有则退出当前函数栈,继续在调用函数的栈中进行查找。
4. 不断重复上述过程。若到达main函数的栈,依旧没有匹配的,则终止程序。
5. 上述这个沿着调用链查找匹配的catch子句的过程称为栈展开。
找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。
以上,我们对于异常处理机制的原理有所了解。
对于大型的程序代码而言,就需要对于自定义类型的异常进行处理。
下面是自定义的类型匹配:
#include <iostream> #include <string> using namespace std; class Exception { public : Exception(int errId, const char * errMsg) : _errId(errId ) , _errMsg(errMsg ) {} void What () const { cout<<"errId:" <<_errId<< endl; cout<<"errMsg:" <<_errMsg<< endl; } private : int _errId ; // 错误码 string _errMsg ; // 错误消息 }; void Func1 (bool isThrow) { // ... if (isThrow ) { throw Exception (1, "抛出 Excepton对象" ); } // ... printf("Func1(%d)\n" , isThrow); } void Func2 (bool isThrowString, bool isThrowInt) { // ... if (isThrowString ) { throw string ("抛出 string对象" ); } // ... if(isThrowInt ) { throw 7; } printf("Func2(%d, %d)\n" , isThrowString, isThrowInt ); } void Func () { try { Func1(false ); Func2(true , true); } catch(const string& errMsg) { cout<<"Catch string Object:" <<errMsg<< endl; } catch(int errId) { cout<<"Catch int Object:" <<errId<< endl; } catch(const Exception& e) { e.What (); } catch(...) { cout<<" 未知异常"<< endl; } printf ("Func()\n"); } int main() { Func(); return 0; }
异常的重新抛出
有可能单个的catch不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理。
class Exception { public : Exception(int errId = 0, const char * errMsg = "" ) : _errId(errId ) , _errMsg(errMsg ) {} void What () const { cout<<"errId:" <<_errId<< endl; cout<<"errMsg:" <<_errMsg<< endl; } private : int _errId ; // 错误码 string _errMsg ; // 错误消息 }; void Func1 () { throw string ("Throw Func1 string"); } void Func2 () { try { Func1(); } catch(string & errMsg) { cout<<errMsg <<endl; //Exception e (1, "Rethorw Exception"); //throw e ; // throw; // throw errMsg; } } void Func3 () { try { Func2(); } catch (Exception & e) { e.What (); } }
异常与构造函数&析构函数
- 构造函数完成对象的构造和初始化,需要保证不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化。
- 析构函数主要完成资源的清理,需要保证不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)
exception类是C++定义的一个标准异常的类,通常我们通过继承exception类定义合适的异常类。
http://www.cplusplus.com/reference/exception/exception/
本文只是简单地从异常的使用场景,介绍了基本使用方法,一些高级的异常用法没有罗列,还有待补充,偏文可能有纰漏,希望大家指出