执行结果为:Enter the dividend: 10
Enter the divisor : 0
Exception is raised:
The divisor cannot be 0!
注意,通过atexit()注册的终止处理函数必须显式(使用return语句)或隐式地正常返回,而不能通过调用exit()或longjmp()等其他方式终止,否则将导致未定义的行为。例如,在GCC4.1.2编译环境下,调用exit()终止时仍等效于正常返回;而VC6.0编译环境下,调用exit()的处理函数将阻止其他已注册的处理函数被调用,并且可能导致程序异常终止甚至崩溃。嵌套调用exit()函数将导致未定义的行为,因此在终止处理函数或信号处理函数中尽量不要调用exit()。abort()函数原型声明如下:#include <stdlib.h>
void abort(void);
该函数将SIGABRT信号发送给调用进程(进程不应忽略此信号)。ISO C规定,调用abort将向主机环境递送一个未成功终止的通知,其方法是调用raise(SIGABRT)函数。因此,abort()函数理论上的实现为:void abort(void)
{
raise(SIGABRT);
exit(EXIT_FAILURE);
}
可见,即使捕捉到SIGABRT信号且相应信号处理程序返回,abort()函数仍然终止程序。Posix.1也说明abort()函数并不理会进程对此信号的阻塞和忽略。进程捕捉到SIGABRT信号后,可在其终止之前执行所需的清理操作(如调用exit)。若进程不在信号处理程序中终止自己,Posix.1声明当信号处理程序返回时,abort()函数终止该进程。ISO C规定,abort()函数是否冲洗输出流、关闭已打开文件及删除临时文件由实现决定。Posix.1则要求若abort()函数终止进程,则它对所有打开标准I/O流的效果应当与进程终止前对每个流调用fclose相同。为提高可移植性,若希望冲洗标准I/O流,则应在调用abort()之前执行这种操作。3.2 断言(assert)abort()和exit()函数无条件终止程序。也可使用断言(assert)有条件地终止程序。assert是诊断调试程序时经常使用的宏,定义在<assert.h>内。该宏的典型实现如下:#ifdef NDEBUG
#define assert(expr) ((void) 0)
#else
extern void __assert((const char *, const char *, int, const char *));
#define assert(expr) \
((void) ((expr) || \
(__assert(#expr, __FILE__, __LINE__, __FUNCTION__), 0)))
#endif
可见,assert宏仅在调试版本(未定义NDEBUG)中有效,且调用__assert()函数。该函数将输出发生错误的文件名、代码行、函数名以及条件表达式:void __assert(const char *assertion, const char * filename,
int linenumber, register const char * function)
{
fprintf(stderr, " [%s(%d)%s] Assertion '%s' failed.\n",
filename, linenumber,
((function == NULL) ? "UnknownFunc" : function),
assertion);
abort();
}
因此,assert宏实际上是一个带有错误说明信息的abort(),并做了前提条件检查。若检查失败(断言表达式为逻辑假),则报告错误并终止程序;否则继续执行后面的语句。使用者也可按需定制assert宏。例如,另一实现版本为:#undef assert
#ifdef NDEBUG
#define assert(expr) ((void) 0)
#else
#define assert(expr) ((void) ((expr) || \
(fprintf(stderr, "[%s(%d)] Assertion '%s' failed.\n", \
__FILE__, __LINE__, #expr), abort(), 0)))
#endif
注意,expr1||expr2表达式作为单独语句出现时,等效于条件语句if(!(expr1))expr2。这样,assert宏就可扩展为一个表达式,而不是一条语句。逗号表达式expr2返回最后一个表达式的值(即0),以符合||操作符的要求。使用断言时应注意以下几点:断言用于检测理论上绝不应该出现的情况,如入参指针为空、除数为0等。对比以下两种情况:char *Strcpy(char *pszDst, const char *pszSrc)
{
char *pszDstOrig = pszDst;
assert((pszDst != NULL) && (pszSrc != NULL));
while((*pszDst++ = *pszSrc++) != '\0');
return pszDstOrig;
}
FILE *OpenFile(const char *pszName, const char *pszMode)
{
FILE *pFile = fopen(pszName, pszMode);
assert(pFile != NULL);
if(NULL == pFile)
return NULL;
//...
return pFile;
}
Strcpy()函数中断言使用正确,因为入参字符串指针不应为空。OpenFile()函数中则不能使用断言,因为用户可能需要检查某个文件是否存在,而这并非错误或异常。2)assert是宏不是函数,在调试版本和非调试版本中行为不同。因此必须确保断言表达式的求值不会产生副作用,如修改变量和改变方法的返回值。不过,可根据这一副作用测试断言是否打开:int main(void)
{
int dwChg = 0;
assert(dwChg = 1);
if(0 == dwChg)
printf("Assertion should be enabled!\n");
return 0;
}
不应使用断言检查公共方法的参数(应使用参数校验代码),但可用于检查传递给私有方法的参数。可使用断言测试方法执行的前置条件和后置条件,以及执行前后的不变性。断言条件不成立时,会调用abort()函数终止程序,应用程序没有机会做清理工作(如关闭文件和数据库)。3.3 封装为减少错误检查和处理代码的重复性,可对函数调用或错误输出进行封装。封装具有错误返回值的函数通常针对频繁调用的基础性系统函数,如内存和内核对象操作等。举例如下:pid_t Fork(void) //首字母大写,以区分系统函数fork()
{
pid_t pid;
if((pid = fork())<0)
{
fprintf(stderr, "Fork error: %s\n", strerror(errno));
exit(0);
}
return pid;
}
Fork()函数出错退出时依赖系统清理资源。若还需清理其他资源(如已创建的临时文件),可增加一个负责清理的回调函数。注意,并非所有系统函数都可封装,应根据具体业务逻辑确定。封装错误输出通常需要使用ISO C变长参数表特性。例如《Unix网络编程》中将输出至标准出错文件的代码封装如下:#include <stdarg.h>
#include <syslog.h>
#define HAVE_VSNPRINTF 1
#define MAXLINE 4096 /* max text line length */
int daemon_proc; /* set nonzero by daemon_init() */
static void err_doit(int errnoflag, int level, const char * fmt, va_list ap)
{
int errno_save, n;
char buf[MAXLINE + 1];
errno_save = errno; /* Value caller might want printed. */
#ifdef HAVE_VSNPRINTF
vsnprintf(buf, MAXLINE, fmt, ap);
#else
vsprintf(buf, fmt, ap); /* This is not safe */
#endif
n = strlen(buf);
if (errnoflag) {
snprintf(buf + n, MAXLINE - n, ": %s", strerror(errno_save));
}
strcat(buf, "\n");
if (daemon_proc) {
syslog(level, buf);
} else {
fflush(stdout); /* In case stdout and stderr are the same */
fputs(buf, stderr);
fflush(stderr);
}
return;
}
void err_ret(const char * fmt, ...)
{
va_list ap;
va_start(ap, fmt);
err_doit(1, LOG_INFO, fmt, ap);
va_end(ap);
return;
} |