zoukankan      html  css  js  c++  java
  • C和C++中的异常处理

     

    1、简介

    许多的编程新手对异常处理视而不见,程序里很少考虑异常情况。一部分人甚至根本就不考虑,以为程序总是能以正确的途径运行。譬如我们有的程序设计者调用fopen打开一个文件后,立马就开始进行读写操作,根本就不考虑文件是否正常打开了。在编程过程中恰当地使用异常处理可以增强软件的健壮性。本文将介绍C和C++对于异常处理的一些常用方法。

    2、C语言的异常处理

    2.1、无条件终止

           标准C库提供了exit()和abort()两个函数,它们可以强行终止程序的运行,其声明处于<stdlib.h>头文件中。这两个函数本身不能检测异常,但在C程序发生异常后经常使用这两个函数进行程序终止。下面的这个例子描述了exit()的行为:

     

    #include <stdio.h>

    #include <stdlib.h>

    int main(void)

    {

    printf("this will be executed ");

    exit(EXIT_SUCCESS);

           printf("this will not be executed ");

           return 0;

    }

    输出结果为:

    this will be executed

    在这个例子中,main函数在输出了“this will be executed”之后就执行了exit函数(此函数原型为void exit(int)),因此,程序不会输出" this will not be executed "。程序中的exit(EXIT_SUCCESS)表示程序正常结束,与之对应的exit(EXIT_FAILURE)表示程序执行错误,只能强行终止。EXIT_SUCCESS、EXIT_FAILURE分别定义为0和1。对于exit函数,我们可以利用atexit函数为exit事件"挂接"另外的函数,这种"挂接"有点类似Windows编程中的"钩子"(Hook)。例如:

    #include <stdio.h>

    #include <stdlib.h>

    static void atExitFunc(void)

    {

           printf("atexit hooked function ");

    }

    int main(void)

    {

           atexit(atExitFunc);

           printf("this will be executed ");

           exit(EXIT_SUCCESS);

           printf("this will not be executed ");

           return 0;

    }

    输出结果为:

    this will be executed

    atexit hooked function

           在这个例子中,main函数在输出了“this will be executed”之后就执行了exit函数,因此程序不会输出" this will not be executed ",但在程序退出前会执行atExitFunc函数,输出“atexit hooked function”。

       如果把上面例子中的“exit(EXIT_SUCCESS);”语句注释掉,

    则输出结果如下:

    this will be executed

    this will not be executed

    atexit hooked function

    这说明,即便是我们不调用exit函数,当程序本身退出时,atexit挂接的函数仍然会被执行。atexit可以被多次执行,并挂接多个函数,这些函数的执行顺序为后挂接的先执行,例如:

    #include <stdio.h>

    #include <stdlib.h>

    static void atExitFunc1(void)

    {

           printf("atexit hooked function 1 ");

    }

    static void atExitFunc2(void)

    {

           printf("atexit hooked function 2 ");

    }

    static void atExitFunc3(void)

    {

           printf("atexit hooked function 3 ");

    }

    int main(void)

    {

           atexit(atExitFunc1);

           atexit(atExitFunc2);

           atexit(atExitFunc3);

           return 0; 

    }

    输出结果为:

    atexit hooked function 3

    atexit hooked function 2

    atexit hooked function 1

                               

           另一个异常终止函数abort(此函数不带参数,原型为void abort(void))会直接退出程序。例如:

    #include <stdio.h>

    #include <stdlib.h>

    int main(void)

    {

           printf("this will be executed ");

           abort();                                             //这里不是用exit来退出

           printf("this will not be executed ");

           return 0;

    }

    在Visual C++中的debug模式运行时弹出下图1所示的对话框。

     

     

    图1

           虽然exit 和abort两个函数在概念上是相联系的,但它们的效果不同:

           abort():程序异常结束。默认情况下,调用abort()导致运行期诊断和程序自毁。它

    可能会也可能不会刷新缓冲区、关闭被打开的文件及删除临时文件,这依赖于你的编译

    器的具体实现。

           exit():文明地结束程序。除了关闭文件和给运行环境返回一个状态码外,exit()还调

    用了你挂接的atexit()处理程序。

           一般调用abort()处理灾难性的程序故障。因为abort()的默认行为是立即终止程序,

    你就必须负责在调用abort()前存储重要数据。

           相反,exit()会执行用atexit()挂接的函数,你可以把作挂接的函数当做虚拟析构器。执行必要的clean up 代码,你可以安全地终止程序而没有留下尾巴。

    2.2、有条件终止

        abort()和exit()让你无条件终止程序。你还可以有条件地终止程序。其实现体系是每

    个程序员所喜爱的诊断工具:断言,定义于<assert.h>。 assert宏在C语言程序的调试中发挥着重要的作用,它用于检测不会发生的情况,表明一旦发生了这样的情况,程序就实际上执行错误了。先来看看assert的宏定义

    #ifdef  NDEBUG

    #define assert(exp)     ((void)0)

    #else

    #ifdef  __cplusplus

    extern "C" {

    #endif

    _CRTIMP void __cdecl _assert(void *, void *, unsigned);

    #ifdef  __cplusplus

    }

    #endif

    #define assert(exp) (void)( (exp) || (_assert(#exp, __FILE__, __LINE__), 0) )

    #endif  /* NDEBUG */

     

    如果程序不在debug模式下,assert宏实际上什么都不做,所以在assert宏中的表达式不能有副作用。看如下的例子:

    for(int i=10;i>-5;i++)

    {

    ……

    assert((i--)!=0)         //这个语句在debug模式下可以起到递减i的作用,但不在

                         //debug模式下时,assert宏什么都不做,递减i的作用将失效

    ……

    }

    在debug模式下,assert宏实际上是对_assert()函数的调用,这个函数通常有如下形式的定义:

    void _assert(int test, char const *test_image,

    char const *file, int line)

    {

           if (!test)

           {

                  printf("Assertion failed: %s, file %s, line %d ",

                  test_image, file, line);

                  abort();

           }

    }

    所以,失败的断言在调用abort()前显示出失败情况的诊断条件、出错的源文件名称和

    行号。它们实际上是一个带说明信息的abort()并做了前提条件检查,如果检查失败,程序中止。例如下列程序:

    #include <stdio.h>

    #include <stdlib.h>

    #include <assert.h>

    char * myStrcpy( char *strDest, const char *strSrc )

    {

           char *address = strDest;

           assert( (strDest != NULL) && (strSrc != NULL) );

           while( (*strDest++ = *strSrc++) != '' );

           return address;

    }

    int main(void)

    {

           myStrcpy(NULL,NULL);

           return 0;

    }

    代码中包含断言assert( (strDest != NULL) && (strSrc != NULL) ),它的意思是源和目的字符串的地址都不能为空,一旦为空,程序实际上就执行错误了,会引发一个abort。

    失败的断言也会弹出如图1所示的对话框,这是因为_assert()函数中也调用了abort()函数,并且控制台输出结果如图2:

     

    图2

    2.3、全局标记(errno)

    errno在C程序中是一个全局变量,这个变量由C运行时库函数设置,用户程序需要在程序发生异常时检测之。C运行库中主要在math.h和stdio.h头文件声明的函数中使用了errno,前者用于检测数学运算的合法性,后者用于检测I/O操作中(主要是文件)的错误,例如:

    #include <errno.h>

    #include <math.h>

    #include <stdio.h>

    int main(void)

    {

           errno = 0;

           if (NULL == fopen("d:\1.txt", "rb"))

           {

                  printf("%d ", errno);

           }

           else

           {

                  printf("%d ", errno);

           }

           return 0;

    }

    在此程序中,如果文件打开失败(fopen返回NULL),证明发生了异常。我们读取error可以获知错误的原因,如果D盘根目录下不存在"1.txt"文件,将输出2,表示文件不存在;在文件存在并正确打开的情况下,将执行到else语句,输出0,证明errno没有被设置。

    Visual C++提供了两种版本的C运行时库。一个版本供单线程应用程序调用,另一个版本供多线程应用程序调用。多线程运行时库与单线程运行时库的一个重大差别就是对于类似errno的全局变量,每个线程单独设置了一个。因此,对于多线程的程序,我们应该使用多线程C运行时库,才能获得正确的error值。

    另外,在使用errno之前,我们最好将其设置为0,即执行errno = 0的赋值语句。

    2.4、其他

    除了上述异常处理方式外,在C语言中还支持非局部跳转(使用setjmp和longjmp)、信号(使用signal、raise)、返回错误值或回传错误值给参数等方式进行一定能力的异常处理,但是其使用不如2.1~2.3节所介绍方式常用,这里就不细研究。


    3、C++异常处理

    3.1、异常处理语法

    C++的异常处理结构为:

     

    try

    {

    //可能引发异常的代码

    }

    catch(type_1 e)

    {

    // type_1类型异常处理

    }

    catch(type_2 e)

    {

    // type_2类型异常处理

    }

    catch (...)//会捕获所有未被捕获的异常,必须最后出现

    {

    }

     

     

    异常的抛出方式使用throw(type e),try、catch和throw都是C++为处理异常而添加的关键字。

    异常处理的过程:

    1、  程序或运行库遇到一个错误状况(在try块中);

    2、  抛出一个异常,程序的运行停止于异常点;

    3、  开始搜索异常处理函数。搜索沿调用栈向上搜索,搜索结束于找到了一个异常申明与异常对象的静态类型相匹配;

    4、  进入相应的异常处理函数;

    5、  异常处理函数结束后,跳到此异常处理函数所在的try 块下面最近的一条语句开始执行。

    看看这个例子:

    #include <stdio.h>

    //定义Point结构体(类)

    typedef struct tagPoint

    {

           int x;

           int y;

    } Point;

    //扔出int异常的函数

    static void f(int n)

    {

           throw 1;

    }

    //扔出Point异常的函数

    static void f(Point point)

    {

           Point p;

           p.x = 0;

           p.y = 0;

           throw p;

    }

    int main()

    {

           Point point;

           point.x = 0;

           point.y = 0;

           try

           {

           f(point); //抛出Point异常

    //     f(1); //抛出int异常

           }

           catch (int e)

           {

                  printf("捕获到int异常:%d ", e);

           }

           catch (Point e)

           {

                  printf("捕获到Point异常:(%d,%d) ", e.x, e.y);

          }

                  printf("terminating, after 'try' block ");  //异常处理后从这里开始接着

                                                         //处理代码。

           return 0;

    }

     

    函数f定义了两个版本:f(int)和f(Point),分别抛出int和Point异常。当main函数的try{…}中调用f(point)时和f(1)时,分别输出:

    捕获到Point异常:(0,0)

    terminating, after 'try' block    

    捕获到int异常:1

    terminating, after 'try' block

     

    C++中,throw抛出异常的特点有:

    (1)可以抛出基本数据类型异常,如int和char等。其中;

    (2)可以抛出复杂数据类型异常,如结构体(在C++中结构体也是类)和类;

    (3)C++的异常处理必须由调用者主动检查。一旦抛出异常,而程序不捕获的话,那么abort()函数就会被调用,弹出如图1所示的对话框,程序被终止;

    (4)可以在函数头后加throw([type-ID-list])给出异常规格,声明其能抛出什么类型的异常。type-ID-list是一个可选项,其中包括了一个或多个类型的名字,它们之间以逗号分隔。如果函数没有异常规格指定,则可以抛出任意类型的异常。

    3.2、标准异常

    下面给出了C++提供的一些标准异常:

    namespace std

    {

    //exception派生

    class logic_error; //逻辑错误,在程序运行前可以检测出来

     

    //logic_error派生

    class domain_error; //违反了前置条件

    class invalid_argument; //指出函数的一个无效参数

    class length_error; //指出有一个超过类型size_t的最大可表现值长度的对象的企图

    class out_of_range; //参数越界

    class bad_cast; //在运行时类型识别中有一个无效的dynamic_cast表达式

    class bad_typeid; //报告在表达试typeid(*p)中有一个空指针p

     

    //exception派生

    class runtime_error; //运行时错误,仅在程序运行中检测到

     

    //runtime_error派生

    class range_error; //违反后置条件

    class overflow_error; //报告一个算术溢出

    class bad_alloc; //存储分配错误

    }

    请注意观察上述类的层次结构,可以看出,标准异常都派生自一个公共的基类exception。基类包含必要的多态性函数提供异常描述,可以被重载。下面是exception类的原型:

    class exception

    {

    public:

    exception() throw();

    exception(const exception& rhs) throw();

    exception& operator=(const exception& rhs) throw();

    virtual ~exception() throw();

    virtual const char *what() const throw();

    };

    其中的一个重要函数为what(),它返回一个表示异常的字符串指针。下面我们从exception类派生一个自己的类:

    #include <iostream>

    #include <exception>

    using namespace std;

    class myexception:public exception

    {

    public:

           myexception():exception("一个重载exception的例子"){}

    };

    int main()

    {

           try

           {

                  throw myexception();

           }

           catch (exception &r) //捕获异常

           {

                  cout << "捕获到异常:" << r.what() << endl;

           }

           return 0;

    }

    程序运行,输出:

    捕获到异常:一个重载exception的例子

     

    一般的,我们直接以基类捕获异常,例如,本例中使用了“catch (exception &r)”,然后根据基类的多态性进行处理,这是因为基类中的what函数是虚函数

     3.3、异常处理函数

           在标准C++中,还定义了数个异常处理的相关函数和类型(包含在头文件<exception>中):

    namespace std

    {

    //EH类型

    class bad_exception;

    class exception;

    typedef void (*terminate_handler)();

    typedef void (*unexpected_handler)();

    // 函数

    terminate_handler set_terminate(terminate_handler) throw();

    unexpected_handler set_unexpected(unexpected_handler) throw();

    void terminate();

    void unexpected();

    bool uncaught_exception();

    }

    其中的terminate相关函数与未被捕获的异常有关,如果一种异常没有被指定catch模块,则将导致terminate()函数被调用,terminate()函数中会调用ahort()函数来终止程序。可以通过set_terminate(terminate_handler)函数为terminate()专门指定要调用的函数,例如:

     

    #include <cstdio>

    #include <exception>

    using namespace std;

    //定义Point结构体(类)

    typedef struct tagPoint

    {

           int x;

           int y;

    } Point;

    //扔出Point异常的函数

    static void f()

    {

           Point p;

           p.x = 0;

           p.y = 0;

           throw p;

    }

    //set_terminate将指定的函数

    void terminateFunc()

    {

           printf("set_terminate指定的函数 ");

    }

    int main()

    {

           set_terminate(terminateFunc);

           try

           {

                  f(); //抛出Point异

           }

           catch (int) //捕获int异常

           {

                  printf("捕获到int异常");

           }

           //Point将不能被捕获到,引发terminateFunc函数被执行

           return 0;

    }

    这个程序将在控制台上输出 "set_terminate指定的函数" 字符串,因为Point类型的异常没有被捕获到。当然,它也会弹出图1所示对话框(因为调用了abort()函数)。

        上述给出的仅仅是一个set_terminate指定函数的例子。在实际工程中,往往使用set_terminate指定的函数进行一些清除性的工作,其后再调用exit(int)函数终止程序。这样,abort()函数就不会被调用了,也不会输出图1所示对话框。


  • 相关阅读:
    Hibernate save, saveOrUpdate, persist, merge, update 区别
    Eclipse下maven使用嵌入式(Embedded)Neo4j创建Hello World项目
    Neo4j批量插入(Batch Insertion)
    嵌入式(Embedded)Neo4j数据库访问方法
    Neo4j 查询已经创建的索引与约束
    Neo4j 两种索引Legacy Index与Schema Index区别
    spring data jpa hibernate jpa 三者之间的关系
    maven web project打包为war包,目录结构的变化
    创建一个maven web project
    Linux下部署solrCloud
  • 原文地址:https://www.cnblogs.com/riskyer/p/3221912.html
Copyright © 2011-2022 走看看