什么叫异常处理?
什么叫异常(Exception)?顾名思义就是非正式的情况,出现了不希望出现的意外,异常处理就是遇到这种意外时准备的对策和解决方案。比如您开着一辆劳斯莱斯在公路上行走,突然前面出现一个小孩,幸好您眼疾手快紧急刹车,而避免了一场交通事故。在这个例子中突然出现的小孩就是异常,紧急刹车就是异常处理(面对这种突发情况采取的解决方案)。
程序来源于现实,是现实的抽象和模拟,也会有异常,因此异常的处理就显得极为重要。试想如果您的手机的某个应用使用两下就崩溃了,或都出现各种意想不到情况,您是不有种想扔手机的冲动,您还会再用这种App吗?
异常处理的解决方案
C++中的异常处理:
C++中的异常处理主要有两种实现方式:(1).返回错误码,(2)try...catch机制捕获异常。
返回错误码
返回错误码是传统的C语言的处理异常的方式。在一个函数中,如果发生某种不该发生的错误或异常,则直接返回一个错误码,函数的调用方在调用该函数的时候根据返回的错误码的类型进行相应的处理。由于C++的历史原因(由C发展而来),为了兼容性,现在的C++程序中仍能看到很多用错误码的方式向上抛出异常信息。
这种方式一般用于对性能的要求较高的地方,常用在系统库和接口的实现中。因此这种方式可以精确控制逻辑,让程序员只关注逻辑和性能,而程序的完整性和健壮性,交给上层调用方去处理。其实现方式如下:
例【1】 对两个浮点数进行除法运算,如果除数为0则抛出错误码。
1、定义错误码 RetCode.h
1 #ifndef RETCODE_H 2 #define RETCODE_H 3 4 typedef long ReturnCode; 5 6 //成功 7 #define RT_OK 0L 8 //失败 9 #define RT_FAILED 1L 10 11 //参数错误 12 #define RT_PARAM_ERROR 2L 13 //无法预知的错误 14 #define RT_UNEXPECTED_ERROR 3L 15 //空指针 16 #define RT_NULL_PTR 4L 17 //分配内存错误 18 #define RT_ALLOCATE_MEMORY_FAILED 5L 19 //不支持的操作 20 #define RT_UNSUPPORT_OPERATE 6L 21 22 23 #endif /* RETCODE_H */
错误码的实现方式:
1 #include "RetCode.h" 2 #include <iostream> 3 4 //判断一个浮点数是否为0 5 #define DEQUALZEOR(X) ((X) <= 0.0001 && (X) > -0.0001) 6 7 //除法运算 8 ReturnCode division(double dividend, double divisor, double& result) 9 { 10 if(DEQUALZEOR(divisor)) 11 return RT_PARAM_ERROR; 12 else 13 { 14 result = dividend / divisor; 15 return RT_OK; 16 } 17 } 18 19 int main(int argc, char** argv) 20 { 21 double r = 0; 22 ReturnCode ret = division(5, 0, r); 23 if(RT_PARAM_ERROR == ret) 24 { 25 std::cout << "参数错误,检查是否除数为0。" << std::endl; 26 } else 27 { 28 std::cout << r << std::endl; 29 } 30 return 0; 31 }
try...catch机制捕获异常
上面这种返回错误码的方式,可能有效地定义和控制各种错误和异常,但是每个调用都要检查错误值,极不方便,也容易让程序规模加倍。其实C++有一种专门的机制用于处理异常,那就是try...catch机制。
try { // 抛出异常,或可能抛出异常的调用 } catch (ExceptioinObject e) { // 处理异常 } catch (...) { // 捕获所有类型的异常 }
说明:
1.try中的代码块用于抛出(throw)异常,或调用可能抛出异常的函数、对象;
2.throw关键字可用于抛出任意类型的对象,可以是类的对象,也可以是内置数据类型的对象(常称为变量),还可以是指针(指针本身就是一个对象,是一个特殊的对象,用于指向另外一个对象的地址);
3.第一个catch括号中的e表示异常对象,这个对象也可以是任意类型的对象。当throw出的对象类型与e的类型相同时,则捕获到异常,进行catch代码块中的异常处理。
4.第二个catch括号中的“...”表示任意类型,可以捕获任意类型的异常。
5.一个try可以对应一个或多个catch,catch子句被检查的顺序与它们在try块之后排列顺序相同,一旦找到一个匹配,则后续的catch子句将不再检查,按此规则,catch_all(catch(...){})表示处理前面所列各种异常之外的异常。
6.catch子句可以包含返回语句(return),也可不包含返回语句。包含返回语句,则整个调用函数结束,后面的语句不再执行。而不包含返回语句,则执行catch列表之后的下一条语句。
异常处理,把正常逻辑和错误处理分离开来,由函数实现方抛出异常,由调用者捕获这个异常,调用者就可以知道程序函数调用出现错误了,并去处理,而是否终止程序就把握在调用者手里了。
我们将用上面的例子用try...catch...方式实现
例【2】对两个浮点数进行除法运算,如果除数为0则抛出异常。
1 #include <iostream> 2 //判断一个浮点数是否为0 3 #define DEQUALZEOR(X) ((X) <= 0.0001 && (X) > -0.0001) 4 5 using namespace std; 6 7 //除数不为0异常 8 class DivisorZeorException{ 9 public: 10 DivisorZeorException(double value) : m_value(value){} 11 12 void showInfo() 13 { 14 cout << "the divisor " << m_value << " is wrong." << endl; 15 } 16 private: 17 double m_value; 18 }; 19 20 double division(double dividend, double divisor) 21 { 22 if ( DEQUALZEOR(divisor) ) 23 { 24 throw DivisorZeorException(divisor); 25 } 26 return dividend / divisor; 27 } 28 29 int main() 30 { 31 try 32 { 33 double result = division(10, 0); 34 cout << "result: " << result << endl; 35 } catch (DivisorZeorException e) 36 { 37 e.showInfo(); 38 } catch(...) 39 { 40 cout << "all exception" << endl; 41 } 42 return 0; 43 }
异常的高级特性
(1)异常规范
可在函数的后面用throw列出可能抛出的异常,并保证该函数不会抛出任何其它类型的异常。如上面的division函数改为:
double division(double dividend, double divisor) throw(DivisorZeorException) { if ( DEQUALZEOR(divisor) ) { throw DivisorZeorException(divisor); } return dividend / divisor; }
如果在运行时,函数抛出了一个没有被列在它的异常规范中的异常(并且函数中所抛出的异常,没有在该函数内部处理),则系统调用C++标准库中定义的函数unexpected()。如果异常规范形式为throw(),则表示不得抛出任何异常。
(2)异常类的继承
异常类也可以继承,在catch捕获异常的时候应按照由子类到父类的顺序,因此catch子句被检查的顺序与它们在try块之后排列顺序相同,所以在catch子句列表中最特化的(匹配条件最严格的,即子类)catch子句必须先出现 。假设有三个异常类,ExceptionC是ExceptionB的子类,ExceptionB是ExceptionA的子类,try...catch...就写成:
try { //可能抛出异常的语句 } catch (ExceptionC c) { //处理ExceptionC异常 } catch (ExceptionB b) { //处理ExceptionB异常 } catch (ExceptionA a) { //处理ExceptionA异常 }
C++标准中的异常类
C++标准中已经定义了一套常用的异常类,它们之间的层次关系如下:
exception是所有异常类的父类,仅仅定义了拷贝构造函数、拷贝赋值运算符、一个虚析构函数和一个名为what的虚成员,what函数返回一个const char*,用于返回一些异常信息。
C++标准中定义的类虽然不多,但我们在定义自己的异常类的时候还是应该尽量利用已有的异常类,至少就继承自exception类,保持结构的统一性。