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\n");

exit(EXIT_SUCCESS);

       printf("this will not be executed\n");

       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\n");

}

int main(void)

{

       atexit(atExitFunc);

       printf("this will be executed\n");

       exit(EXIT_SUCCESS);

       printf("this will not be executed\n");

       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\n");

}

static void atExitFunc2(void)

{

       printf("atexit hooked function 2\n");

}

static void atExitFunc3(void)

{

       printf("atexit hooked function 3\n");

}

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\n");

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

       printf("this will not be executed\n");

       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\n",

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

       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\n", errno);

       }

       else

       {

              printf("%d\n", 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\n", e);

       }

       catch (Point e)

       {

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

      }

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

                                                     //处理代码。

       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指定的函数\n");

}

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


posted on 2013-07-28 22:05  you Richer  阅读(415)  评论(0编辑  收藏  举报