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
在这几个步骤中表现出了不同的特点。
C
语言异常处理
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) {
atexit(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) {
atexit(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
在
Visual C++
中,如果以
abort
函数(此函数不带参数,原型为
void abort(void)
)终止程序,则会在
debug
模式运行时弹出对话框。
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 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
。
失败的断言也会弹出对话框,这是因为
_assert()
函数中也调用了
abort()
函数。
assert
本质上是一个宏,而不是一个函数,因而不能把带有副作用的表达式放入
assert
的
"
参数
"
中。
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
的赋值语句。
4
其它
除了上述异常处理方式外,在
C
语言中还支持非局部跳转(使用
setjmp
和
longjmp
)、信号(使用
signal
、
raise
)、返回错误值或回传错误值给参数等方式进行一定能力的异常处理。
从以上分析可知,
C
语言的异常处理是简单而不全面的。与
C++
的异常处理比起来,
C
语言异常处理相形见绌。
C++
异常处理语法
标准
C++
语言中专门集成了异常处理的相关语法(与之不同的是,所有的
C
标准库异常体系都需要运行库的支持,它不是语言内核支持的)。当然,异常处理被加到程序设计语言中,也是程序语言发展和逐步完善的必然结果。
C++
不是唯一集成异常处理的语言。
1 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
标准异常
下面给出了
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 *wh
}
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()
函数就不会被调用了,也不会输出对话框。
MFC
异常处理
MFC
中异常处理的语法和语义构建在标准
C++
异常处理语法和语义的基础之上,其解决方案为:
MFC
异常处理
= MFC
异常处理类
+
宏
1
宏
MFC
定义了
TRY
、
CATCH
(及
AND_CATCH
、
END_CATCH
)和
THROW
(及
THROW_LAST
)等用于异常处理的宏,其本质上也是标准
C++
的
try
、
catch
和
throw
的进一步强化,由这些宏的定义可知:
#ifndef _AFX_OLD_EXCEPTIONS #define TRY { AFX_EXCEPTION_LINK _afxExceptionLink; try { #define CATCH(class, e) } catch (class* e) / { ASSERT(e->IsKindOf(RUNTIME_CLASS(class))); / _afxExceptionLink.m_pException = e; #define AND_CATCH(class, e) } catch (class* e) / { ASSERT(e->IsKindOf(RUNTIME_CLASS(class))); / _afxExceptionLink.m_pException = e; #define END_CATCH } } #define THROW(e) throw e #define THROW_LAST() (AfxThrowLastCleanup(), throw) // Advanced macros for smaller code #define CATCH_ALL(e) } catch (CException* e) / { { ASSERT(e->IsKindOf(RUNTIME_CLASS(CException))); / _afxExceptionLink.m_pException = e;
#define AND_CATCH_ALL(e) } catch (CException* e) / { { ASSERT(e->IsKindOf(RUNTIME_CLASS(CException))); / _afxExceptionLink.m_pException = e;
#define END_CATCH_ALL } } }
#define END_TRY } catch (CException* e) / { ASSERT(e->IsKindOf(RUNTIME_CLASS(CException))); / _afxExceptionLink.m_pException = e; } }
这些宏在使用语法上,有如下特点:
(
1
)用
TRY
块包含可能产生异常的代码;
(
2
)用
CATCH
块检测并处理异常。要注意的是,
CATCH
块捕获到的不是异常对象,而是指向异常对象的指针。此外,
MFC
靠动态类型来辨别异常对象;
(
3
)可以在一个
TRY
块上捆绑多个异常处理捕获块,第一次捕获使用宏
CATCH
,以后的使用
AND_CATCH
,而
END_CATCH
则用来结束异常捕获队列;
(
4
)在异常处理程序内部,可以用
THROW_LAST
再次抛出最近一次捕获的异常。
2 MFC
异常处理类
MFC
较好地将异常封装到
CException
类及其派生类中,自成体系,下表给出了
MFC
提供的预定义异常:
异常类
含义
CMemoryException
内存不足
CFileException
文件异常
CArchiveException
存档
/
序列化异常
CNotSupportedException
响应对不支持服务的请求
CResourceException Windows
资源分配异常
CDaoException
数据库异常(
DAO
类)
CDBException
数据库异常(
ODBC
类)
COleException OLE
异常
COleDispatchException
调度(自动化)异常
CUserException
用消息框警告用户然后引发一般
CException
的异常
标准
C++
的异常处理可以处理任意类型的异常,而上节的
MFC
宏则只能处理
CException
的派生类型,下面我们看一个
CFileException
的使用例子:
#include <iostream.h> #include "afxwin.h" int main() {
TRY
{
CFile f( "d://1.txt", CFile::modeWrite );
}
CATCH( CFileException, e )
{
if( e->m_cause == CFileException::fileNotFound )
cout << "ERROR: File not found/n" << endl;
}
}
要想这个程序能正确地执行,我们可以在第一个
__try
块的外面再套一个
__try
块和一个接收
filter-expression
返回值为
EXCEPTION_EXECUTE_HANDLER
的
__except
块,程序改为:
#include "stdio.h" void main() {
int* p = NULL; //
定义一个空指针
puts("SEH begin");
__try
{
__try
{
puts("in try");
__try
{
puts("in try");
*p = 0; //
引发一个内存访问异常
}
__finally
{
puts("in finally");
}
}
__except(puts("in filter"), 0)
{
puts("in except");
}
}
__except(puts("in filter"), 1)
{
puts("in except");
}
puts("SEH end"); }
程序输出:
SEH begin
in try //
执行
__try
块
in try //
执行嵌入的
__try
块
in filter1 //
执行
filter-expression
,返回
EXCEPTION_CONTINUE_SEARCH in filter2 //
执行
filter-expression
,返回
EXCEPTION_EXECUTE_HANDLER in finally //
展开嵌入的
__finally in except2 //
执行对应的
__except
块
SEH end //
处理完毕
由此可以看出,因为第一个
__except
的
filter-expression
返回
EXCEPTION_CONTINUE_SEARCH
的原因,
"in except1"
没有被输出。程序之所以没有崩溃,是因为最终碰到了接收
EXCEPTION_EXECUTE_HANDLER
的第
2
个
__except
。
SEH
使用复杂的地方在于较难控制异常处理的流动方向,弄不好程序就
"
挂
"
了。如果把例
4-1
中的
__except(puts("in filter"), 1)
改为
__except(puts("in filter"), -1)
,程序会进入一个死循环,输出:
SEH begin in try //
执行
__try
块
in try //
执行嵌入的
__try
块
in filter //
执行
filter-expression
,返回
EXCEPTION_CONTINUE_EXECUTION in filter in filter in filter in filter
…
//
疯狂输出
"in filter"
最后疯狂地输出
"in filter"
,我们把断点设置在
__except(puts("in filter"), -1)
语句之前,按
F5
会不断进入此断点。
各种异常处理的对比
下表给出了从各个方面对这本文所给出的
Visual C++
所支持的四种异常处理进行的对比:
异常处理
支持语言
是否标准
复杂度
推荐使用
C
异常处理
C
语言
标准
C
简单
推荐
C++
异常处理
C++
语言
标准
C++
较简单
推荐
MFC
异常处理
C++
语言
仅针对
MFC
程序
较简单
不推荐
SEH
异常处理
C
和
C++
语言
仅针对
Microsoft
编译环境
较复杂
不推荐
转载请注明原文地址: https://ibbs.8miu.com/read-21263.html