1.C语言对于错误的处理
首先举一个例子
#include <iostream> #include <setjmp.h> 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; } }
错误处理方式看起来耦合度不是很高,正常代码和错误处理的代码分离了,处理处理的代码都汇聚在一起了。但是基于这种局部跳转的方式来处理代码,在 C++中却存在很严重的问题,那就是对象不能被析构,局部跳转后不会主动去调用已经实例化对象的析构函数。这将导致内存泄露的问题。下面这个例子充分显示 了这点:
#include <iostream> #include <csetjmp> 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; } }
2.C++语言对于错误的处理
2.1异常的匹配符合参数原则
C++异常处理机制是一个用来有效地处理运行错误的非常强大且灵活的工具,它提供了更多的弹性、安全性和稳固性,克服了传统方法所带来的问题。异常的抛出和处理主要使用了以下三个关键字: try、 throw 、 catch 。
try-catch语句形式如下 : try { 包含可能抛出异常的语句; } catch(类型名 [形参名]) // 捕获特定类型的异常 { } catch(类型名 [形参名]) // 捕获特定类型的异常 { } catch(...) // 三个点则表示捕获所有类型的异常 { }下面我们看看类型匹配的问题:
#include <iostream> using namespace std; int main() { try{ throw 'a'; }catch(int a) { cout << "int" << endl; }catch(char c) { cout << "char" << endl; } }
上面的代码的输出结果是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(); } }
匹配原则:
- 允许非常量到常量的类型转换,也就是说可以抛出一个非常量类型,然后使用catch捕捉对应的常量类型版本
- 允许从派生类到基类的类型转换
- 允许数组被转换为数组指针,允许函数被转换为函数指针
2.2重新抛出
为什么要重新抛出异常?实际工程中我们可以对第三方库中抛出的异常进行捕获、重新解释(统一异常类型,方便代码问题定位),然后再抛出。格式如下所示:try{ throw Exception("I am a exception"); }catch(...) { //log the exception throw; }
如果我们在代码中抛出一个异常,但是我不使用任何catch语句来捕获异常,执行上面的程序会出现下面的结果:
terminate called after throwing an instance of 'MyError' Aborted (core dumped)
为什么会出现这样的结果呢?当我们抛出一个异常的时候,异常会随着函数调用关系,一级一级向上抛出,直到被捕获才会停止。如果最终没有被捕获将会导致调用terminate函数,上面的输出就是自动调用terminate函数导致的,为了保证更大的灵活性,C++提供了set_terminate函数可以用来设置自己的terminate函数。设置完成后,抛出的异常如果没有被捕获就会被自定义的terminate函数进行处理。
1.定义一个无返回值无参数的函数(函数类型为void(*)()类型)
a)不能抛出异常b)必须以某种方式结束当前程序(abort/exit/…)
2.调用set_terminate注册自定义的terminate()函数
a)返回值为默认的terminate函数入口地址。#include <iostream> #include <cstdlib> #include <exception> using namespace std; void my_terminate() { cout << "void my_terminate()" << endl; exit(1); } class Test { public: Test() { cout << "Test()"; cout << endl; } ~Test() { cout << "~Test()"; cout << endl; } }; int main() { set_terminate(my_terminate); static Test t; throw 1; return 0; }注意:实际编程过程中应该避免在构造函数中抛出异常,如果没有办法避免,那么一定要 在构造函数中对其进行捕获进行处理
2.3main函数、构造函数中的初始化列表抛出的异常捕获
#include <iostream> using namespace std; int main() try { throw "main"; } catch(const char* msg) { cout << msg << endl; return 1; } //main函数语句块,可以捕获main函数中抛出的异常. class Base { public: Base(int data,string str)try:m_int(data),m_string(str)//对初始化列表中可能会出现的异常也会进行捕捉 { // some initialize opt }catch(const char* msg) { cout << "catch a exception" << msg << endl; } private: int m_int; string m_string; }; int main() { Base base(1,"zhangyifei"); }
2.4常见异常处理
动态内存分配错误
分配动态内存使用的是new和new[]操作符,如果他们分配内存失败,就会抛出bad_alloc异常,在new头文件中,所以我们的代码中应该捕捉这些异常。常见的代码形式如下:
try { //其他代码 ptr = new int[num_max]; //其他代码 } catch(bad_alloc &e) { //这里常见的处理方式为:先释放已经分配的内存,然后结束程序,或者打印一条错误信息并继续执行 }可以使用类似C语言的方式处理,但这时要使用的nothrow版本,使用"new (nothrow)"的形式分配内存。这时,如果分配不成功,返回的是NULL指针,而不再是抛出bad_alloc异常。也可以定制内存分配失败行为。C++允许指定一个new处理程序(newhandler)回调函数。默认的并没有new处理程序,如果我们设置了new处理程序,那么当new和new[]分配内存失败时,会调用我们设定的new处理程序,而不是直接抛出异常。通过set_new_handler函数来设置该回调函数。要求被回调的函数没有返回值,也没有形式参数。