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