深入浅出话异常-(1)
深入浅出话异常-(1)
Robert Schmidt
May 10, 1999
本期讨论要点: 标准C的异常处理机制。
前言
标准C提供了几种异常管理机制,这些机制在标准C++里也可用,但是相关的头文件名称作了改变:旧的标准C头文件名会从<name.h>映射到新的标准C++里的头文件名<cname>。(头文件名的前缀C是为了记忆,指明它们是标准C的库文件)
虽然在C++的向后兼容里保留了C的头文件,但我劝告你在任何可能的地方使用新的头文件。对于许多实际使用中,最大的改变是在新的头文件与namespace std内进行声明。请看以下三种不同类型的示例:
//旧的使用使用方法,在标准C++里被替换成#include <cstdio>
#include <stdio.h>
FILE *f = fopen("blarney.txt", "r");
//现在的用法,与旧方法很相似
std::FILE *f = std::fopen("blarney.txt", "r");
//混合使用,Visual C++支持这种用法?????
#include <cstdio>
using namespace std;
FILE *f = fopen("blarney.txt", "r");
不幸的是,Microsoft's Visual C++不能在新的头文件与namespace std同时具备的条件下进行声明,即使这种行为是标准C必需的。除非等到Visual C++支持这种行为,我将在本行使用旧的C风格名字。
(对于像Microsft这样的库供应商来说,实现这些C库头文件的正确性需要维护与测试两套不同的代码,这是一项艰巨的任务,且不带来任何商业价值)
一、绝对终止:
这是一种彻底忽略异常的方法,大概这种简单的响应是一种安全的退出方法。在一些情形里,这是最正确的方法。
C库头文件<stdlib.h>提供了两个不是相当完美的函数:abort与exit,两者都不返回它的调用者,并且结束程序运行。
虽然两者在概念上是相同的,但使用它们的结果是不同的:
- abort: 粗鲁地结束程序。这是默认的,在运行时诊断里调用abort来安全结束程序。这种结束方式可能会或可能不会刷新与关闭打开的文件或删除临时文件。
- exit:文明地结束程序。它附加了关闭打开的文件与返回状态码给执行环境,exit还调用你用atexit注册的回调函数。
你通常是在发生严重异常的情况下调用abort,由于abort默认行为是立即结束程序,你需要在调用abort之前保存你的数据。(在讨论<signal.h>里会再提到)
对于两者的差异,exit执行客户用atexit注册的清除代码,它们的调用顺序是按它们被注册的相反顺序来的。示例:
#include <stdio.h>#include <stdlib.h>static void atexit_handler_1(void) { printf("within 'atexit_handler_1'
"); }static void atexit_handler_2(void) { printf("within 'atexit_handler_2'
"); }int main(void) { atexit(atexit_handler_1); atexit(atexit_handler_2); exit(EXIT_SUCCESS); printf("this line should never appear
"); return 0; }/* 运行后的结果: within 'atexit_handler_2' within 'atexit_handler_1' 并返回退出码给调用环境.*/(注意:如果你的程序在main函数结束时没有显式调用exit,那么你用atexit注册的处理函数也会被调用)。
二、条件结束:
abort与exit无条件终止你的程序。你也可以有条件地结束你的程序,这种机制是每一个程序员喜受的诊断工具:assert宏定义在<assert.h>,如下相似代码:
#if defined NDEBUG #define assert(condition) ((void) 0)#else #define assert(condition) _assert((condition), #condition, __FILE__, __LINE__)#endif
//译注:各家产品提供的assert的实现并不一样,比如:
Visual C++ 6.0的实现是:#define assert(exp) (void)((exp)||(_assert(#exp, __FILE__, __LINE__), 0));
Borland C++ 5.5的实现是:#define assert(exp) ((exp) ? (void)0 : _assert(#exp, __FILE__, __LINE__))
至于函数_assert(在gcc的库中_assert是一个宏)是各家的内部实现,不一定得非要_assert这个名字,其内容一般是利用printf函数(在WIN平台上往往是调用MessageBox)输出出错信息(文件名及行号)并调用abort终止程序。//end 译注
在这个定义里,当定义了预处理符号NDEBUG的时候,断言是无效的,这意味着assert断言宏只在你的Debug版本中有效。在Release版本里,assert断言宏不进行任何计算。由于这个而会引起一些侧面效应,比如:
/* 调试版本 */#undef NDEBUG#include <assert.h>#include <stdio.h>int main(void) { int i = 0; assert(++i != 0); printf("i is %d
", i); return 0; }/* 当运行后输出: i is 1*/那么现在改变代码版本到release版本,定义NDEBUG:
/* release版本*/#defing NDEBUG#include <assert.h>#include <stdio.h>int main(void) { int i = 0; assert(++i != 0); printf("i is %d
", i); return 0; } /* 当运行后输出: i is 0*/所以在assert中只能是比较而不能有实质性的动作,否则调试和发布版的结果可能会大相径庭。
因此,为了避免这种差异,确保在assert表达式不能包含有侧面影响的代码。
只在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,我给这种机制的示例是相当的粗糙;你的库实现者可能更复杂。
assert典型是用在调试逻辑错误,它永远不会存在于release程序里。
static void f(int *p) { assert(p != NULL);//这儿! /* ... */ }在使用assert中要注意逻辑错误与运行时错误的区别:
/* ...让用户输入文件名... */FILE *file = fopen(name, mode);assert(file != NULL); /* 相当可疑的用法??? */
这种错误出现在assert表达式里,但它不是BUG,它是运行时异常,assert可能会不正确地响应,你应该使用其它机制,我在下面介绍。
三、非局部goto:
对比于abort与exit,goto 让你有更多地管理异常的方法,不幸的是gotos是局部的,goto只能在它们函数的内部跳转,因此不能在程序的任意地方控制它。
为了克服这种限制,标准C提供了setjmp与longjmp函数,它可以goto到任何地方。头文件 <setjmp.h>定义了这些函数,包括间接的jmp_buf,

