Visual C++中的异常处理浅析

    Visual C++提供了对C语言、C++语言及MFC的支持,因而涉及到的异常(exception)处理也包含了这三种类型,即C语言、C++语言和MFC的异常处理。除此之外,微软对C和C++的异常处理进行了扩展,提出了结构化异常处理(SEH)的概念,它支持C和C++(与之相比,MFC异常处理仅支持C++)。

    一个典型的异常处理包含如下几个步骤:

    (1) 程序执行时发生错误;

    (2) 以一个异常对象(最简单的是一个整数)记录错误的原因及相关信息;

    (3) 程序检测到这个错误(读取异常对象);

    (4) 程序决定如何处理错误;

    (5) 进行错误处理,并在此后恢复/终止程序的执行。

    C、C++、MFC及SEH在这几个步骤中表现出了不同的特点。本文将对这四种异常处理进行介绍,并对它们进行对比分析。本文例程的调试平台为Visual C++6.0, 操作系统为Windows XP,所有程序均调试通过。

    在进入正式的讲解之前,先说几句废话。许多的编程新手对异常处理视而不见,程序很少考虑异常情况。一部人甚至根本就不考虑,以为程序总是能以正确的途径运行。譬如,我们有的程序设计者调用fopen打开一个文件后,立马就开始进行读写操作,根本就不考虑文件是否正常打开了。这种习惯一定要改掉,纵使你再不愿意!这是软件健壮性的需要!异常处理不浪费时间!

    1.  C语言异常处理

    1.1  异常终止

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

    #include <stdio.h>

    #include <stdlib.h>

    int main(void)

    {

        exit(EXIT_SUCCESS);

        printf("程序不会执行到这里/n");

        return 0;

    }

    在这个例子中,main函数一开始就执行了exit()函数(此函数原型为void exit(int)),因此,程序不会输出"程序不会执行到这里"。程序中的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挂接的函数/n");

    }

    int main(void)

    {

        ateixt(atExitFunc);

        exit(EXIT_SUCCESS);

        printf("程序不会执行到这里/n");

        return 0;

    }

    程序输出"atexit挂接的函数"后即终止。来看下面的程序,我们不调用exit函数,看看atexit挂接的函数是否执行:

    #include <stdio.h>

    #include <stdlib.h>

    static void atExitFunc(void)

    {

        printf("atexit挂接的函数/n");

    }

    int main(void)

    {

        ateixt(atExitFunc);

        //exit(EXIT_SUCCESS);

        printf("不调用exit函数/n");

        return 0;

    }

    程序输出:

    不调用exit函数

    atexit挂接的函数

    这说明,即便是我们不调用exit函数,当程序本身退出时,atexit挂接的函数仍然会被执行

    atexit可以被多次执行,并挂接多个函数,这些函数的执行顺序为后挂接的先执行,例如:

    #include <stdio.h>

    #include <stdlib.h>

    static void atExitFunc1(void)

    {

        printf("atexit挂接的函数1/n");

    }

    static void atExitFunc2(void)

    {

        printf("atexit挂接的函数2/n");

    }

    static void atExitFunc3(void)

    {

        printf("atexit挂接的函数3/n");

    }

    int main(void)

    {

        atexit(atExitFunc1);

        atexit(atExitFunc2);

        atexit(atExitFunc3);

        return 0;

    }

    输出结果是:

    atexit挂接的函数3

    atexit挂接的函数2

    atexit挂接的函数1

    1.2  断言(assert)

    assert宏在C语言程序的调试中发挥这重要的作用,它用于检测不会发生的情况,表明一旦发生了这样的情况,程序实际上就执行错误了,例如strcpy函数:

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

    {

        char* address = strDest;

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

        while( (*strDest++ = *strSrc++)!= '/0' )

        ;

        return address;

    }

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

    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

    #define  assert(exp) (void)( (exp) || (_assert(#exp,  _FILE_,  _LINE_),  0) )

    #endif /*NDEBUG*/

    如果程序不在debug模式下,assert宏实际上什么都不做;而在debug模式下,实际上是对_assert()函数的调用,此函数将输出发生错误的文件名、代码行、条件表达式。例如下列程序:

    #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++)!='/0' );

        return address;

    }

    int main(void)

    {

        myStrcpy(NULL, NULL);

        return 0;

    }

    在此程序中,为了避免我们的strcpy与C库中的strcpy重名,将其改为myStrcpy。程序输出如下错误:

    Assertion Failed: (strDest!=NULL) && (strSrc!=NULL) , fiel D:/vcproject/exception/exception/main.c  line=7

    abnormal program termination

    press any key to continue

    一定要记住的是assert本质是一个宏,而不是一个函数,因而不能把带有副作用的表达式放入assert的“参数”中。

    1.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),证明发生了异常。我们读取errno可以获知错误的原因,如果D盘根目录下不存在"1.txt"文件,将输出2,表示文件不存在;在文件存在并正确打开的情况下,将执行到else语句,输出0,证明errno没有被设置。

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

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

    1.4  其它

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

    从以上分析可知,C语言的异常处理是简单而不全面的。与C++的异常处理比起来,C语言异常处理相形见绌,它就像娘胎里的雏婴。

    2.  C++语言异常处理

    2.1 C++异常处理语法

    感谢C++语言的后期改造者们,他们在标准C++语言中专门集成了异常处理的相关语法(与之不同的是,所有的C标准库异常体系都需要运行库的支持,它不是语言内核支持的)。当然,异常处理被加到程序设计语言中,也是程序语言发展和逐步完善的必然结果。我们看到,C++不是唯一集成异常处理的语言。

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

    try

    {

        //可能引发异常的代码

    }

    catch(type_1 e)

    {

        //type_1类型异常处理

    }

    catch(type_2 e)

    {

        //type_2类型异常处理

    }

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

    {

    }

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

    #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/n", e);

        }

        catch(Point e)

        {

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

        }

        return 0;

    }

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

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

    和

    捕获到int异常:1

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

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

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

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

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

    2.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函数是虚函数。

    2.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()函数会调用abort()函数来终止程序可以通过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()

    {

        Ponit p;

        p.x = 0;

        p.y = 0;

        throw p;

    }

    //set_terminate将指定的函数

    void  terminateFunc()

    {

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

    }

    int main()

    {

        set_terminat(terminateFunc);

        try

        {

            f(); //抛出Ponit异常

        }

        catch(int) //捕获int异常

        {

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

        }

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

        return 0;

    }

    这个程序将在控制台上输出"set_terminate指定的函数"字符串,因为Point类型的异常没有被捕获到。

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

    关于标准C++的异常处理,还包含一些比较复杂的技巧和内容,我们可以查阅《more effective C++》的条款9~条款15。

    3.  MFC异常处理

    略。

    4.  结构化异常处理

    结构化异常处理(Structured Exception Handling, 简称SEH)是微软针对Windows程序异常处理进行的扩展,在Visual C++中,它同时支持C和C++语言。SEH不宜与标准C++异常处理和MFC异常处理混用,对于C++程序,微软建议使用标准C++的异常处理。

    为了支持SEH,Visual C++中定义了四个关键字(由于这些关键字是非标准关键字,其它编译器不一定支持),用以扩展C和C++语言:

    (1)  __except

    (2)  __finally

    (3)  __leave

    (4)  __try

    其基本语法为:

    __try

    {

        ...//可能导致异常的被监控代码块

    }

    __except(filter-expression)

    {

        ..//异常处理函数

    }

    或:

    __try

    {

        ...

    }

    __finally

    {

        ...//终止

    }

    其执行步骤如下:

    (1) __try块被执行;

    (2) 如果try块没有出现异常,则执行到__except块之后;否则,执行到__except块,根据filter-expression的值决定异常处理方法:

    a.  filter-expression的值为EXCEPTION_CONTINUE_EXECUTION(-1)

    恢复异常,从发生异常处下面开始执行,异常处理函数本身不被执行;

    b.  filter-expression的值为EXCEPTION_CONTINUE_SEARCH(0)

    异常不被识别,终止异常,从异常发生处开始退栈,一路上遇到的终止函数都被执行。

    c.  filter-expression的值为EXCEPTION_EXECUTE_HANDLER(1)

    异常被识别,终止异常,从异常发生处开始退栈,一路上遇到的终止函数都被执行。

    看看这个例子:

    略。

    5.  各种异常处理的对比

    下表给出了从各个方面对本文所给出的Visual C++所支持的四种异常处理进行的对比:  

异常处理

支持语言

是否标准

复杂度

推荐度

C异常处理

C语言

标准C

简单

推荐

C++异常处理

C++语言

标准C++

较简单

推荐

MFC异常处理

C++语言

仅针对MFC程序

较简单

不推荐

SHE异常处理

CC++语言

仅针对Microsoft编译环境

较复杂

不推荐

    本文所讲解的仅仅是Visual C++异常处理的初步知识,对于更深入的内容,还需要我们在不断的编程过程中去领悟和学习。

    在程序设计过程中,我们不能嫌异常处理“麻烦”,对可能的错误视而不见,不加考虑。因为比敏啊了异常处理的“麻烦”,将会给我们的程序带来更大的“麻烦”。而程序中包含的必要的异常处理,也是对一位优秀程序员的基本要求。

 

文章出处:http://dev.yesky.com/115/2158115.shtml

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值