• 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所示对话框。


  • 相关阅读:
    php 上传大文件主要涉及配置upload_max_filesize和post_max_size两个选项
    Linux 文件系统IO性能优化【转】
    MOOC Linux内核之旅小结【转】
    python实战===教你用微信每天给女朋友说晚安【转】
    wxpy: 用 Python 玩微信【转】
    AMBA总线协议AHB、APB、AXI对比分析【转】
    高手进阶,终极内存技术指南——完整/进阶版 II (转)【转】
    ARMCC和GCC编译ARM代码的软浮点和硬浮点问题 【转】
    程序员必知之浮点数运算原理详解【转】
    Hash算法【转】
  • 原文地址:https://www.cnblogs.com/riskyer/p/3221912.html
Copyright © 2020-2023  润新知