C++中的异常处理

来源:互联网 发布:上海海辉软件 编辑:程序博客网 时间:2024/06/08 16:40

C++中的异常是指存在于运行时的反常行为,这些行为超出了函数正常功能的范围。典型的异常包括失去数据库,失去数据库连接以及遇到意外输入等。当程序某部分检测到一个它无法处理的问题时,需要用到异常处理。
我们来看传统的错误处理方式,在以后的处理中避免用到:
1.终止程序(例如atol,atoi,输入NULL,会产生段错误,导致程序异常退出,如果没有core文件)

2.返回一个表示错误的值(很多系统函数都是这样,例如malloc内存不足,分配失败时,返回NULL指针)

3.返回一个合法值,让程序处于某种非法的状态(这个真的很坑)

4.调用一个预先准备好在出现”错误”的情况下用的函数(比较少用,且回调代码不应该多出现)。

在C++中,我们通过抛出一条表达式来引发一个异常。被抛出的表达式的类型以及当前的 调用链共同决定了哪段处理代码将用来处理该异常。异常处理包括:

throw表达式:异常检测部分使用throw表达式来表示它遇到了无法处理的问题,我们说throw引发了异常。当执行一个throw时,跟在throw后面的语句将不再被执行。程序的控制权从throw转移到与之匹配的catch模块,该catch可能是同一个函数中的局部catch,也可能是位于直接或间接调用了发生异常的函数的另一个函数中。控制权从一处转移到另一处,有两个重要意义:
(1),沿着函数调用链的函数可能会提早结束
(2),一旦程序开始执行异常处理代码,则沿着调用链创建的对象将被销毁。

try语句块:异常处理部分用try语句块处理异常。try语句块以关键字try开始,并以一个或多个catch子句结束。当选中了某个catch子句处理异常之后,执行与之对应的块,catch一旦完成,程序跳转到try语句块最后一个catch子句之后的那条语句继续执行。

一套异常类:用于在throw表达式和相关的catch子句之间传递异常的具体信息。

下面举例说明:

#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>using namespace std;#include<string>class Exception{public:    Exception(int id, const char *msg = "")        :_errid(id)        , _errmsg(msg){}    void What()const    {        cout << _errid << endl;        cout << _errmsg << endl;    }protected:    int _errid;    string _errmsg;};void F2(){    char* p1 = (char*)malloc(0x7fffffff);    if (p1 == 0)    {        string msg("申请内存失败");        throw msg;    }    cout << "F2()" << endl;}//void F2()//{//  FILE *fout = fopen("test.txt", "r");//文件实际不存在//  if (fout == NULL)//  {//      throw  Exception(1, "打开文件失败");//  }//}void F1(){    F2();}int main(){    try    {        F1();    }    catch (string msg)    {        cout << msg << endl;    }    catch (Exception e)    {        e.What();    }    system("pause\n");    return 0;}

异常规范:
1、成员函数在类内声明和类外定义两处必须有相同的异常规范。
2、函数抛出一个没有被列在它异常规范中的异常时(且函数中抛出异常没有在函数内部进行处理),
系统调用C++标准库中定义的函数unexpected().
3、如果异常规范为throw(),则表示不得抛出任何异常,该函数不用放在try块中。
4、派生类的虚函数的异常规范必须与基类虚函数的异常规范一样或更严格(是基类虚函数的异常
的子集)。因为:派生类的虚函数被指向基类类型的指针调用时,保证不会违背基类成员函数的异常规范。

栈展开
如果对抛出异常的函数的调用语句位于一个try语句内,则检查与该try块关联的catch子句。如果找到了匹配的catch, 就使用该catch处理异常。否则,如果该try语句嵌套在其他try块中,则继续检查与外层try匹配的catch语句。如果没找到匹配的catch,程序将调用标准库函数terminate,终止程序的执行过程。这个过程成为栈展开。如果在栈展开过程中退出了某个块,编译器将确保在这个块中创建的对象被正确的销毁。如果某个局部对象的类型是类类型,则该对象的析构函数将被自动调用。

异常与构造函数和析构函数
(1) 如果异常发生在构造函数中,则当前的对象可能只构造了一部分,有的成员已经初始化了,所以需要保证不要在构造函数中抛出异常,否则可能会导致对象不完整或不完全初始化。
(2)析构函数在栈展开的过程中执行,如果异常抛出后没有被正常捕获,则系统调用terminate函数终止程序,则析构函数不会被执行,而析构函数主要完成资源的清理,需要保证不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)

捕获异常
当进入一个catch语句后,通过异常对象初始化异常声明中的参数,和函数的参数类似。如果catch的参数类型是非引用类型,则该参数是异常对象的一个副本,在catch语句内改变该参数实际上改变的是局部副本而非异常对象本身;相反,如果参数时引用类型,则和其他引用参数一样,该参数是异常对象的一个别名,此时改变参数也就是改变异常对象。
如果catch的参数是基类类型,则我们可以使用其派生类类型的异常对象对其初始化。注意:如果catch接收的异常与某个继承体系有关,则最好将该catch的参数定义成引用类型。

异常捕获的匹配规则:
1. 允许从非const对象到const的转换。
2. 允许从派生类型到基类类型的转换。
3. 将数组转换为指向数组类型的指针,将函数转换为指向函数类型的指针。
我们来用程序验证第二点:

#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>using namespace std;#include<string>class Exception{public:    Exception(int id = 0,const char* msg = "")        :_errId(id)        , _errMsg(msg){}    virtual const char* What() = 0;    int GetErrId()    {        return _errId;    }protected:    int _errId;    string _errMsg;};class OutOfRange :public Exception{public:    OutOfRange(const char* msg = "")//越界        :Exception(1, msg){}    virtual const char* What()    {        _errMsg += "越界";        return _errMsg.c_str();    }};class BadAlloc :public Exception//申请内存失败{public:    BadAlloc(const char* msg = "")        :Exception(2, msg){}    virtual const  char* What()    {        _errMsg += "申请内存失败";        return _errMsg.c_str();    }};class OverFlow :public Exception//栈溢出{};template<class T, size_t N = 100>class Array{public:    T& operator[](size_t index)    {        if (index >= N)        {            throw OutOfRange(__FUNCTION__);        }        return _a[index];    }protected:    T _a[N];};//new[]template<class T>T* NewArray(size_t N){    T* p = (T*)malloc(sizeof(T)*N);    if (p == 0)    {        throw BadAlloc("模拟实现new[]的newArry抛异常");    }    for (size_t i = 0; i < N; ++i)    {        new(p + i)T();//定位new表达式    }    return p;}int main(){    try    {        /*Array<int, 10>a1;        a1[0] = 0;        a1[10] = 10;*/        //string* p = NewArray<string>(10);        char* p1 = NewArray<char>(0x7fffffff);    }    catch (Exception& e)    {        cout << e.What() << endl;    }    system("pause\n");    return 0;}

经过不同情况下异常的处理调试,以上程序执行结果如下:
这里写图片描述

异常的重新抛出
有可能单个的catch不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理。即当捕获异常后还需要处理其他事情时,我们需要用到异常的重新抛出。

void F1(){    string *p1 = new string[10];    char* p2 = NULL;    try    {         p2= new char[0x7ffffff];    }    catch (...)    {        delete[] p1;        throw;//异常的重新抛出,否则在捕获异常后不进行后续操作,会造成内存泄漏    }    delete[] p1;    delete[] p2;}int main(){    try    {        F1();    }    catch (exception& e)    {        cout << e.what() << endl;    }    system("pause\n");    return 0;}

使用异常的优缺点:
优点:1,比返回错误码的方式更清晰的表示程序错误原因。
2,很多C++第三方库使用异常,使用异常能更好的用这些库。
3,引用异常能更好的测试代码,如测试框架使用异常
缺点:
1,执行乱码,不方便测试跟踪代码
2,异常安全带的问题,需要更好的使用RAII编写异常安全的代码,如智能指针。

1 0