异常处理
增强错误恢复能力是提高代码健壮性的最有力的途径之一,C语言中采用的错误处理方法被认为是紧耦合的,函数的使用者必须在非常靠近函数调用的地方编写错误处理代码,这样会使得其变得笨拙和难以使用。C++中引入了异常处理机制,这是C++的主要特征之一,是考虑问题和处理错误的一种更好的方式。使用错误处理可以带来一些优点,如下:
错误处理代码的编写不再冗长乏味,并且不再和正常的代码混合在一起,程序员只需要编写希望产生的代码,然后在后面某个单独的区段里编写处理错误的嗲吗。多次调用同一个函数,则只需要某个地方编写一次错误处理代码。
错误不能被忽略,如果一个函数必须向调用者发送一次错误信息。它将抛出一个描述这个错误的对象。
传统的错误处理和异常处理
在讨论异常处理之前,我们先谈谈C语言中的传统错误处理方法,这里列举了如下三种:
在函数中返回错误,函数会设置一个全局的错误状态标志。
使用信号来做信号处理系统,在函数中raise信号,通过signal来设置信号处理函数,这种方式耦合度非常高,而且不同的库产生的信号值可能会发生冲突
使用标准C库中的非局部跳转函数 setjmp和longjmp ,这里使用setjmp和longjmp来演示下如何进行错误处理:
#include #include jmp_buf static_buf; //用来存放处理器上下文,用于跳转 void do_jmp() { //do something,simetime occurs a little error //调用longjmp后,会载入static_buf的处理器信息,然后第二个参数作为返回点的setjmp这个函数的返回值 longjmp(static_buf,10);//10是错误码,根据这个错误码来进行相应的处理 } int main() { int ret = 0; //将处理器信息保存到static_buf中,并返回0,相当于在这里做了一个标记,后面可以跳转过来 if((ret = setjmp(static_buf)) == 0) { //要执行的代码 do_jmp(); } else { //出现了错误 if (ret == 10) std::cout << "a little error" << std::endl; } }</div>
错误处理方式看起来耦合度不是很高,正常代码和错误处理的代码分离了,处理处理的代码都汇聚在一起了。但是基于这种局部跳转的方式来处理代码,在C++中却存在很严重的问题,那就是对象不能被析构,局部跳转后不会主动去调用已经实例化对象的析构函数。这将导致内存泄露的问题。下面这个例子充分显示了这点
#include #include using namespace std; class base { public: base() { cout << "base construct func call" << endl; } ~base() { cout << "~base destruct func call" << endl; } }; jmp_buf static_buf; void test_base() { base b; //do something longjmp(static_buf,47);//进行了跳转,跳转后会发现b无法析构了 } int main() { if(setjmp(static_buf) == 0) { cout << "deal with some thing" << endl; test_base(); } else { cout << "catch a error" << endl; } }</div>
在上面这段代码中,只有base类的构造函数会被调用,当longjmp发生了跳转后,b这个实例将不会被析构掉,但是执行流已经无法回到这里,b这个实例将不会被析构。这就是局部跳转用在C++中来处理错误的时候带来的一些问题,在C++中异常则不会有这些问题的存在。那么接下来看看如何定义一个异常,以及如何抛出一个异常和捕获异常吧.
异常的抛出
class MyError { const char* const data; public: MyError(const char* const msg = 0):data(msg) { //idle } }; void do_error() { throw MyError("something bad happend"); } int main() { do_error(); }</div>
上面的例子中,通过throw抛出了一个异常类的实例,这个异常类,可以是任何一个自定义的类,通过实例化传入的参数可以表明发生的错误信息。其实异常就是一个带有异常信息的类而已。异常被抛出后,需要被捕获,从而可以从错误中进行恢复,那么接下来看看如何去捕获一个异常吧。在上面这个例子中使用抛出异常的方式来进行错误处理相比与之前使用局部跳转的实现来说,最大的不同之处就是异常抛出的代码块中,对象会被析构,称之为堆栈反解.
异常的捕获
C++中通过catch关键字来捕获异常,捕获异常后可以对异常进行处理,这个处理的语句块称为异常处理器。下面是一个简单的捕获异常的例子:
try{ //do something throw string("this is exception"); } catch(const string& e) { cout << "catch a exception " << e << endl; }</div>
catch有点像函数,可以有一个参数,throw抛出的异常对象,将会作为参数传递给匹配到到catch,然后进入异常处理器,上面的代码仅仅是展示了抛出一种异常的情况,加入try语句块中有可能会抛出多种异常的,那么该如何处理呢,这里是可以接多个catch语句块的,这将导致引入另外一个问题,那就是如何进行匹配。
异常的匹配
异常的匹配我认为是符合函数参数匹配的原则的,但是又有些不同,函数匹配的时候存在类型转换,但是异常则不然,在匹配过程中不会做类型的转换,下面的例子说明了这个事实:
#include using namespace std; int main() { try{ throw 'a'; }catch(int a) { cout << "int" << endl; }catch(char c) { cout << "char" << endl; } }</div>
上面的代码的输出结果是char,因为抛出的异常类型就是char,所以就匹配到了第二个异常处理器。可以发现在匹配过程中没有发生类型的转换。将char转换为int。尽管异常处理器不做类型转换,但是基类可以匹配到派生类这个在函数和异常匹配中都是有效的,但是需要注意catch的形参需要是引用类型或者是指针类型,否则会 导致切割派生类这个问题。
//基类 class Base{ public: Base(string msg):m_msg(msg) { } virtual void what(){ cout << m_msg << endl; } void test() { cout << "I am a CBase" << endl; } protected: string m_msg; }; //派生类,重新实现了虚函数 class CBase : public Base { public: CBase(string msg):Base(msg) { } void what() { cout << "CBase:" << m_msg << endl; } }; int main() { try { //do some thing //抛出派生类对象 throw CBase("I am a CBase exception"); }catch(Base& e) { //使用基类可以接收 e.what(); } }</div>
上面的这段代码可以正常的工作,实际上我们日常编写自己的异常处理函数的时候也是通过继承标准异常来实现字节的自定义异常的,但是如果将Base&换成Base的话,将会导致对象被切割,例如下面这段代码将会编译出错,因为CBase被切割了,导致CBase中的test函数无法被调用。
try { //do some thing throw CBase("I am a CBase exception"); }catch(Base e) { e.test(); }</div>
到此为此,异常的匹配算是说清楚了,总结一下,异常匹配的时候基本上遵循下面几条规则:
异常匹配除了必须要是严格的类型匹配外,还支持下面几个类型转换.
允许非常量到常量的类型转换,也就是说可以抛出一个非常量类型,然后使用catch捕捉对应的常量类型版本
允许从派生类到基类的类型转换
允许数组被转换为数组指针,允许函数被转换为函数指针
假想一种情况,当我要实现一代代码的时候,希望无论抛出什么类型的异常我都可以捕捉到,目前来说我们只能写上一大堆的catch语句捕获所有可能在代码中出现的异常来解决这个问题,很显然这样处理起来太过繁琐,幸好C++提供了一种可以捕捉任何异常的机制,可以使用下列代码中的语法。
catch(...) {
//异常处理器,这里可以捕捉任何异常,带来的问题就是无法或者异常信息
}
如果你要实现一个函数库,你捕捉了你的函数库中的一些异常,但是你只是记录日志,并不去处理这些异常,处理异常的事情会交给上层调用的代码来处理.对于这样的一个场景C++也提供了支持.
try{ throw Exception("I am a exception"); }catch(...) { //log the exception throw; }</div>
通过在catch语句块中加入一个throw,就可以把当前捕获到的异常重新抛出.在异常抛出的那一节中,我在代码中抛出了一个异常,但是我没有使用任何catch语句来捕获我抛出的这个异常,执行上面的程序会出现下面的结果.
terminate called after throwing an instance of 'MyError'
Aborted (core dumped)
为什么会出现这样的结果呢?,当我们抛出一个异常的时候,异常会随着函数调用关系,一级一级向上抛出,直到被捕获才会停止,如果最终没有被捕获将会导致调用terminate函数,上