网站建设开票税收分类,怎么里ip做网站,上海政务服务网官网,网络营销师是做什么的点击蓝字关注我们本文主要总结嵌入式系统C语言编程中#xff0c;主要的错误处理方式。一、错误概念1.1 错误分类从严重性而言#xff0c;程序错误可分为致命性和非致命性两类。对于致命性错误#xff0c;无法执行恢复动作#xff0c;最多只能在用户屏幕上打印出错消息或将其… 点击蓝字关注我们本文主要总结嵌入式系统C语言编程中主要的错误处理方式。一、错误概念1.1 错误分类从严重性而言程序错误可分为致命性和非致命性两类。对于致命性错误无法执行恢复动作最多只能在用户屏幕上打印出错消息或将其写入日志文件然后终止程序而对于非致命性错误多数本质上是暂时的(如资源短缺)一般恢复动作是延迟一些时间后再次尝试。从交互性而言程序错误可分为用户错误和内部错误两类。用户错误呈现给用户通常指明用户操作上的错误而程序内部错误呈现给程序员(可能携带用户不可接触的数据细节)用于查错和排障。应用程序开发者可决定恢复哪些错误以及如何恢复。例如若磁盘已满可考虑删除非必需或已过期的数据若网络连接失败可考虑短时间延迟后重建连接。选择合理的错误恢复策略可避免应用程序的异常终止从而改善其健壮性。1.2 处理步骤错误处理即处理程序运行时出现的任何意外或异常情况。典型的错误处理包含五个步骤程序执行时发生软件错误。该错误可能产生于被底层驱动或内核映射为软件错误的硬件响应事件(如除零)。以一个错误指示符(如整数或结构体)记录错误的原因及相关信息。程序检测该错误(读取错误指示符或由其主动上报程序决定如何处理错误(忽略、部分处理或完全处理)恢复或终止程序的执行。上述步骤用C语言代码表述如下int func()
{int bIsErrOccur 0;//do something that might invoke errorsif(bIsErrOccur) //Stage 1: error occurredreturn -1; //Stage 2: generate error indicator//...return 0;
}int main(void)
{if(func() ! 0) //Stage 3: detect error{//Stage 4: handle error}//Stage 5: recover or abortreturn 0;
}调用者可能希望函数返回成功时表示完全成功失败时程序恢复到调用前的状态(但被调函数很难保证这点)。二 、错误传递2.1 返回值和回传参数C语言通常使用返回值来标志函数是否执行成功调用者通过if等语句检查该返回值以判断函数执行情况。常见的几种调用形式如下if((p malloc(100)) NULL)//...if((c getchar()) EOF)//...if((ticks clock()) 0)//...Unix系统调用级函数(和一些老的Posix函数)的返回值有时既包括错误代码也包括有用结果。因此上述调用形式可在同一条语句中接收返回值并检查错误(当执行成功时返回合法的数据值)。返回值方式的好处是简便和高效但仍存在较多问题代码可读性降低没有返回值的函数是不可靠的。但若每个函数都具有返回值为保持程序健壮性就必须对每个函数进行正确性验证即调用时检查其返回值。这样代码中很大一部分可能花费在错误处理上且排错代码和正常流程代码搅在一起比较混乱。质量降级条件语句相比其他类型的语句潜藏更多的错误。不必要的条件语句会增加排障和白盒测试的工作量。信息有限通过返回值只能返回一个值因此一般只能简单地标志成功或失败而无法作为获知具体错误信息的手段。通过按位编码可变通地返回多个值但并不常用。字符串处理函数可参考IntToAscii()来返回具体的错误原因并支持链式表达char *IntToAscii(int dwVal, char *pszRes, int dwRadix)
{if(NULL pszRes)return Arg2Null;if((dwRadix 2) || (dwRadix 36))return Arg3OutOfRange;//...return pszRes;
}定义冲突不同函数在成功和失败时返回值的取值规则可能不同。例如Unix系统调用级函数返回0代表成功-1代表失败新的Posix函数返回0代表成功非0代表失败标准C库中isxxx函数返回1表示成功0表示失败。无约束性调用者可以忽略和丢弃返回值。未检查和处理返回值时程序仍然能够运行但结果不可预知。新的Posix函数返回值只携带状态和异常信息并通过参数列表中的指针回传有用的结果。回传参数绑定到相应的实参上因此调用者不可能完全忽略它们。通过回传参数(如结构体指针)可返回多个值也可携带更多的信息。综合返回值和回传参数的优点可对Get类函数采用返回值(含有用结果)方式而对Set类函数采用返回值回传参数方式。对于纯粹的返回值可按需提供如下解析接口typedef enum{S_OK, //成功S_ERROR, //失败(原因未明确)通用状态S_NULL_POINTER, //入参指针为NULLS_ILLEGAL_PARAM, //参数值非法通用S_OUT_OF_RANGE, //参数值越限S_MAX_STATUS //不可作为返回值状态仅作枚举最值使用
}FUNC_STATUS;#define RC_NAME(eRetCode) \((eRetCode) S_OK ? Success : \((eRetCode) S_ERROR ? Failure : \((eRetCode) S_NULL_POINTER ? NullPointer : \((eRetCode) S_ILLEGAL_PARAM ? IllegalParas : \((eRetCode) S_OUT_OF_RANGE ? OutOfRange : \Unknown)))))当返回值错误码来自下游模块时可能与本模块错误码冲突。此时建议不要将下游错误码直接向上传递以免引起混乱。若允许向终端或文件输出错误信息则可详细记录出错现场(如函数名、错误描述、参数取值等)并转换为本模块定义的错误码再向上传递。2.2 全局状态标志(errno)Unix系统调用或某些C标准库函数出错时通常返回一个负值并设置全局整型变量errno为一个含有错误信息的值。例如open函数出错时返回-1并设置errno为EACESS(权限不足)等值。C标准库头文件errno.h中定义errno及其可能的非零常量取值(以字符E开头)。在ANSI C中已定义一些基本的errno常量操作系统也会扩展一部分(但其对错误描述仍显匮乏)。Linux系统中出错常量在errno(3)手册页中列出可通过man 3 errno命令查看。除EAGAIN和EWOULDBLOCK取值相同外POSIX.1指定的所有出错编号取值均不同。Posix和ISO C将errno定义为一个可修改的整型左值(lvalue)可以是包含出错编号的一个整数或是一个返回出错编号指针的函数。以前使用的定义为extern int errno;但在多线程环境中多个线程共享进程地址空间每个线程都有属于自己的局部errno(thread-local)以避免一个线程干扰另一个线程。例如Linux支持多线程存取errno将其定义为extern int *__errno_location(void);
#define errno (*__errno_location())函数__ errno_location在不同的库版本下有不同的定义在单线程版本中直接返回全局变量errno的地址而在多线程版本中不同线程调用__errno_location返回的地址则各不相同。C运行库中主要在math.h(数学运算)和stdio.h(I/O操作)头文件声明的函数中使用errno。使用errno时应注意以下几点函数返回成功时允许其修改errno。例如调用fopen函数新建文件时内部可能会调用其他库函数检测是否存在同名文件。而用于检测文件的库函数在文件不存在时可能会失败并设置errno。这样 fopen函数每次新建一个事先并不存在的文件时即使没有任何程序错误发生(fopen本身成功返回)errno也仍然可能被设置。因此调用库函数时应先检测作为错误指示的返回值。仅当函数返回值指明出错时才检查errno值//调用库函数
if(返回错误值)//检查errno库函数返回失败时不一定会设置errno取决于具体的库函数。errno在程序开始时设置为0任何库函数都不会将errno再次清零。因此在调用可能设置errno的运行库函数之前最好先将errno设置为0。调用失败后再检查errno的值。使用errno前应避免调用其他可能设置errno的库函数。如if (somecall() -1)
{printf(somecall() failed\n);if(errno ...) { ... }
}somecall()函数出错返回时设置errno。但当检查errno时其值可能已被printf()函数改变。若要正确使用somecall()函数设置的errno须在调用printf()函数前保存其值if (somecall() -1)
{int dwErrSaved errno;printf(somecall() failed\n);if(dwErrSaved ...) { ... }
}类似地当在信号处理程序中调用可重入函数时应在其前保存其后恢复errno值。使用现代版本的C库时应包含使用errno.h头文件在非常老的Unix 系统中可能没有该头文件此时可手工声明errno(如extern int errno)。C标准定义strerror和perror两个函数以帮助打印错误信息。#include string.h
char *strerror(int errnum);该函数将errnum(即errno值)映射为一个出错信息字符串并返回指向该字符串的指针。可将出错字符串和其它信息组合输出到用户界面或保存到日志文件中如通过fprintf(fp, somecall failed(%s), strerror(errno))将错误消息打印到fp指向的文件中。perror函数将当前errno对应的错误消息的字符串输出到标准错误(即stderr或2)上。#include stdio.h
void perror(const char *msg);该函数首先输出由msg指向的字符串(用户自己定义的信息)后面紧跟一个冒号和空格然后是当前errno值对应的错误类型描述最后是一个换行符。未使用重定向时该函数输出到控制台上若将标准错误输出重定向到/dev/null则看不到任何输出。注意perror()函数中errno对应的错误消息集合与strerror()相同。但后者可提供更多定位信息和输出方式。两个函数的用法示例如下int main(int argc, char** argv)
{errno 0;FILE *pFile fopen(argv[1], r);if(NULL pFile){printf(Cannot open file %s(%s)!\n, argv[1], strerror(errno));perror(Open file failed);}else{printf(Open file %s(%s)!\n, argv[1], strerror(errno));perror(Open file);fclose(pFile);}return 0;
}执行结果为[wangxiaoyuan_localhost test1]$ ./GlbErr /sdb1/wangxiaoyuan/linux_test/test1/test.c
Open file /sdb1/wangxiaoyuan/linux_test/test1/test.c(Success)!
Open file: Success
[wangxiaoyuan_localhost test1]$ ./GlbErr NonexistentFile.h
Cannot open file NonexistentFile.h(No such file or directory)!
Open file failed: No such file or directory
[wangxiaoyuan_localhost test1]$ ./GlbErr NonexistentFile.h test
Open file failed: No such file or directory
[wangxiaoyuan_localhost test1]$ ./GlbErr NonexistentFile.h 2 test
Cannot open file NonexistentFile.h(No such file or directory)!也可仿照errno的定义和处理定制自己的错误代码int *_fpErrNo(void)
{static int dwLocalErrNo 0;return dwLocalErrNo;
}#define ErrNo (*_fpErrNo())
#define EOUTOFRANGE 1
//define other error macros...int Callee(void)
{ErrNo 1;return -1;
}int main(void)
{ErrNo 0;if((-1 Callee()) (EOUTOFRANGE ErrNo))printf(Callee failed(ErrNo:%d)!\n, ErrNo);return 0;
}借助全局状态标志可充分利用函数的接口(返回值和参数表)。但与返回值一样它隐含地要求调用者在调用函数后检查该标志而这种约束同样脆弱。此外全局状态标志存在重用和覆盖的风险。而函数返回值是无名的临时变量由函数产生且只能被调用者访问。调用完成后即可检查或拷贝返回值然后原始的返回对象将消失而不能被重用。又因为无名返回值不能被覆盖。2.3 局部跳转(goto)使用goto语句可直接跳转到函数内的错误处理代码处。以除零错误为例double Division(double fDividend, double fDivisor)
{return fDividend/fDivisor;
}
int main(void)
{int dwFlag 0;if(1 dwFlag){RaiseException:printf(The divisor cannot be 0!\n);exit(1);}dwFlag 1;double fDividend 0.0, fDivisor 0.0;printf(Enter the dividend: );scanf(%lf, fDividend);printf(Enter the divisor : );scanf(%lf, fDivisor);if(0 fDivisor) //不太严谨的浮点数判0比较goto RaiseException;printf(The quotient is %.2lf\n, Division(fDividend, fDivisor));return 0;
}执行结果如下[wangxiaoyuan_localhost test1]$ ./test
Enter the dividend: 10
Enter the divisor : 0
The divisor cannot be 0!
[wangxiaoyuan_localhost test1]$ ./test
Enter the dividend: 10
Enter the divisor : 2
The quotient is 5.00虽然goto语句会破坏代码结构性但却非常适用于集中错误处理。伪代码示例如下CallerFunc()
{if((ret CalleeFunc1()) 0);goto ErrHandle;if((ret CalleeFunc2()) 0);goto ErrHandle;if((ret CalleeFunc3()) 0);goto ErrHandle;//...return;ErrHandle://Handle Error(e.g. printf)return;
}2.4 非局部跳转(setjmp/longjmp)局部goto语句只能跳到所在函数内部的标号上。若要跨越函数跳转需要借助标准C库提供非局部跳转函数setjmp()和longjmp()。它们分别承担非局部标号和goto的作用非常适用于处理发生在深层嵌套函数调用中的出错情况。“非局部跳转”是在栈上跳过若干调用帧返回到当前函数调用路径上的某个函数内。#include setjmp.h
int setjmp(jmp_buf env);
void longjmp(jmp_buf env,int val);函数setjmp()将程序运行时的当前系统堆栈环境保存在缓冲区env结构中。初次调用该函数时返回值为0。longjmp()函数根据setjmp()所保存的env结构恢复先前的堆栈环境即“跳回”先前调用setjmp时的程序执行点。此时setjmp()函数返回longjmp()函数所设置的参数val值程序将继续执行setjmp调用后的下一条语句(仿佛从未离开setjmp)。参数val为非0值若设置为0则setjmp()函数返回1。可见setjmp()有两类返回值用于区分是首次直接调用(返回0)和还是由其他地方跳转而来(返回非0值)。对于一个setjmp可有多个longjmp因此可由不同的非0返回值区分这些longjmp。举个简单例子说明 setjmp/longjmp的非局部跳转jmp_buf gJmpBuf;
void Func1(){printf(Enter Func1\n);if(0)longjmp(gJmpBuf, 1);
}
void Func2(){printf(Enter Func2\n);if(0)longjmp(gJmpBuf, 2);
}
void Func3(){printf(Enter Func3\n);if(1)longjmp(gJmpBuf, 3);
}int main(void)
{int dwJmpRet setjmp(gJmpBuf);printf(dwJmpRet %d\n, dwJmpRet);if(0 dwJmpRet){Func1();Func2();Func3();}else{switch(dwJmpRet){case 1:printf(Jump back from Func1\n);break;case 2:printf(Jump back from Func2\n);break;case 3:printf(Jump back from Func3\n);break;default:printf(Unknown Func!\n);break;}}return 0;
}执行结果为dwJmpRet 0
Enter Func1
Enter Func2
Enter Func3
dwJmpRet 3
Jump back from Func3当setjmp/longjmp嵌在单个函数中使用时可模拟PASCAL语言中嵌套函数定义(即函数内中定义一个局部函数)。当setjmp/longjmp跨越函数使用时可模拟面向对象语言中的异常(exception) 机制。模拟异常机制时首先通过setjmp()函数设置一个跳转点并保存返回现场然后使用try块包含那些可能出现错误的代码。可在try块代码中或其调用的函数内通过longjmp()函数抛出(throw)异常。抛出异常后将跳回setjmp()函数所设置的跳转点并执行catch块所包含的异常处理程序。以除零错误为例jmp_buf gJmpBuf;
void RaiseException(void)
{printf(Exception is raised: );longjmp(gJmpBuf, 1); //throw跳转至异常处理代码printf(This line should never get printed!\n);
}
double Division(double fDividend, double fDivisor)
{return fDividend/fDivisor;
}
int main(void)
{double fDividend 0.0, fDivisor 0.0;printf(Enter the dividend: );scanf(%lf, fDividend);printf(Enter the divisor : );if(0 setjmp(gJmpBuf)) //try块{scanf(%lf, fDivisor);if(0 fDivisor) //也可将该判断及RaiseException置于Division内RaiseException();printf(The quotient is %.2lf\n, Division(fDividend, fDivisor));}else //catch块(异常处理代码){printf(The divisor cannot be 0!\n);}return 0;
}执行结果为Enter the dividend: 10
Enter the divisor : 0
Exception is raised: The divisor cannot be 0!通过组合使用setjmp/longjmp函数可对复杂程序中可能出现的异常进行集中处理。根据longjmp()函数所传递的返回值来区分处理各种不同的异常。使用setjmp/longjmp函数时应注意以下几点必须先调用setjmp()函数后调用longjmp()函数以恢复到先前被保存的程序执行点。若调用顺序相反将导致程序的执行流变得不可预测很容易导致程序崩溃。longjmp()函数必须在setjmp()函数的作用域之内。在调用setjmp()函数时它保存的程序执行点环境只在当前主调函数作用域以内(或以后)有效。若主调函数返回或退出到上层(或更上层)的函数环境中则setjmp()函数所保存的程序环境也随之失效(函数返回时堆栈内存失效)。这就要求setjmp()不可该封装在一个函数中若要封装则必须使用宏(详见《C语言接口与实现》“第4章 异常与断言”)。通常将jmp_buf变量定义为全局变量以便跨函数调用longjmp。通常存放在存储器中的变量将具有longjmp时的值而在CPU和浮点寄存器中的变量则恢复为调用setjmp时的值。因此若在调用setjmp和longjmp之间修改自动变量或寄存器变量的值当setjmp从longjmp调用返回时变量将维持修改后的值。若要编写使用非局部跳转的可移植程序必须使用volatile属性。使用异常机制不必每次调用都检查一次返回值但因为程序中任何位置都可能抛出异常必须时刻考虑是否捕捉异常。在大型程序中判断是否捕捉异常会是很大的思维负担影响开发效率。相比之下通过返回值指示错误有利于调用者在最近出错的地方进行检查。此外返回值模式中程序的运行顺序一目了然对维护者可读性更高。因此应用程序中不建议使用setjmp/longjmp“异常处理”机制(除非库或框架)。2.5 信号(signal/raise)在某些情况下主机环境或操作系统可能发出信号(signal)事件指示特定的编程错误或严重事件(如除0或中断等)。这些信号本意并非用于错误捕获而是指示与正常程序流不协调的外部事件。为处理信号需要使用以下信号相关函数#include signal.h
typedef void (*fpSigFunc)(int);
fpSigFunc signal(int signo, fpSigFunc fpHandler);
int raise(int signo);其中参数signo是Unix系统定义的信号编号(正整数)不允许用户自定义信号。参数fpHandler是常量SIG_DFL、常量SIG_IGN或当接收到此信号后要调用的信号处理函数(signal handler)的地址。若指定SIG_DFL则接收到此信号后调用系统的缺省处理函数若指定SIG_ IGN则向内核表明忽略此信号(SIGKILL和SIGSTOP不可忽略)。某些异常信号(如除数为零)不太可能恢复此时信号处理函数可在程序终止前正确地清理某些资源。信号处理函数所收到的异常信息仅是一个整数(待处理的信号事件)这点与setjmp()函数类似。signal()函数执行成功时返回前次挂接的处理函数地址失败时则返回SIG_ERR。信号通过调用raise()函数产生并被处理函数捕获。以除零错误为例void fphandler(int dwSigNo)
{printf(Exception is raised, dwSigNo%d!\n, dwSigNo);
}
int main(void)
{if(SIG_ERR signal(SIGFPE, fphandler)){fprintf(stderr, Fail to set SIGFPE handler!\n);exit(EXIT_FAILURE);}double fDividend 10.0, fDivisor 0.0;if(0 fDivisor){raise(SIGFPE);exit(EXIT_FAILURE);}printf(The quotient is %.2lf\n, fDividend/fDivisor);return 0;
}执行结果为Exception is raised, dwSigNo8!(0.0不等同于0因此系统未检测到浮点异常)。若将被除数(Dividend)和除数(Divisor)改为整型变量int main(void)
{if(SIG_ERR signal(SIGFPE, fphandler)){fprintf(stderr, Fail to set SIGFPE handler!\n);exit(EXIT_FAILURE);}int dwDividend 10, dwDivisor 0;double fQuotient dwDividend/dwDivisor;printf(The quotient is %.2lf\n, fQuotient);return 0;
}则执行后循环输出Exception is raised, dwSigNo8!。这是因为进程捕捉到信号并对其进行处理时进程正在执行的指令序列被信号处理程序临时中断它首先执行该信号处理程序中的指令。若从信号处理程序返回(未调用exit或longjmp)则继续执行在捕捉到信号时进程正在执行的正常指令序列。因此每次系统调用信号处理函数后异常控制流还会返回除0指令继续执行。而除0异常不可恢复导致反复输出异常。规避方法有两种将SIGFPE信号变成系统默认处理即signal(SIGFPE, SIG_DFL)。此时执行输出为Floating point exception。利用setjmp/longjmp跳过引发异常的指令jmp_buf gJmpBuf;
void fphandler(int dwSigNo)
{printf(Exception is raised, dwSigNo%d!\n, dwSigNo);longjmp(gJmpBuf, 1);
}
int main(void)
{if(SIG_ERR signal(SIGFPE, SIG_DFL)){fprintf(stderr, Fail to set SIGFPE handler!\n);exit(EXIT_FAILURE);}int dwDividend 10, dwDivisor 0;if(0 setjmp(gJmpBuf)){double fQuotient dwDividend/dwDivisor;printf(The quotient is %.2lf\n, fQuotient);}else{printf(The divisor cannot be 0!\n);}return 0;
}注意在信号处理程序中还可使用sigsetjmp/siglongjmp函数进行非局部跳转。相比setjmp函数sigsetjmp函数增加一个信号屏蔽字参数。三 错误处理3.1 终止(abort/exit)致命性错误无法恢复只能终止程序。例如当空闲堆管理程序无法提供可用的连续空间时(调用malloc返回NULL)用户程序的健壮性将严重受损。若恢复的可能性渺茫则最好终止或重启程序。标准C库提供exit()和abort()函数分别用于程序正常终止和异常终止。两者都不会返回到调用者中且都导致程序被强行结束。exit()及其相似函数原型声明如下#include stdlib.h
void exit(int status);
void _Exit(int status);
#include unistd.h
void _exit(int status);其中exit和_Exit由ISO C说明而_exit由Posix.1说明。因此使用不同的头文件。ISO C定义_ Exit旨在为进程提供一种无需运行终止处理程序(exit handler)或信号处理程序(signal handler)而终止的方法是否冲洗标准I/O流则取决于实现。Unix系统中_ Exit 和_ exit同义两者均直接进入内核而不冲洗标准I/O流。_exit函数由exit调用处理Unix特定的细节。exit()函数首先调用执行各终止处理程序然后按需多次调用fclose函数关闭所有已打开的标准I/O流(将所有缓冲的输出数据冲洗写到文件上)然后调用_exit函数进入内核。标准函数库中有一种“缓冲I/O(buffered I/O)”机制。该机制对于每个打开的文件在内存中维护一片缓冲区。每次读文件时会连续读出若干条记录下次读文件时就可直接从内存缓冲区中读取每次写文件时也仅仅写入内存缓冲区等满足一定条件(如缓冲区填满或遇到换行符等特定字符)时再将缓冲区内容一次性写入文件。通过尽可能减少read和write调用的次数该机制可显著提高文件读写速度但也给编程带来某些麻烦。例如向文件内写入一些数据时若未满足特定条件数据会暂存在缓冲区内。开发者并不知晓这点而调用_ _ exit()函数直接关闭进程导致缓冲区数据丢失。因此若要保证数据完整性必须调用exit()函数或在调用_ _ exit()函数前先通过fflush()函数将缓冲区内容写入指定的文件。例如调用printf函数(遇到换行符\n时自动读出缓冲区中内容)函数后再调用exitint main(void)
{printf(Using exit...\n);printf(This is the content in buffer);exit(0);printf(This line will never be reached\n);
}执行输出为Using exit...
This is the content in buffer(结尾无换行符)调用printf函数后再调用_exitint main(void)
{printf(Using _exit...\n);printf(This is the content in buffer);fprintf(stdout, Standard output stream);fprintf(stderr, Standard error stream);//fflush(stdout);_exit(0);
}执行输出为Using _exit...
Standard error stream(结尾无换行符)若取消fflush句注释则执行输出为Using _exit...
Standard error streamThis is the content in bufferStandard output stream(结尾无换行符)通常标准错误是不带缓冲的打开至终端设备的流(如标准输入和标准输出)是行缓冲的(遇换行符则执行I/O操作)其他所有流则是全缓冲的(填满标准I/O缓冲区后才执行I/O操作)。三个exit函数都带有一个整型参数status称之为终止状态(或退出状态)。该参数取值通常为两个宏即EXIT_SUCCESS(0)和EXIT_FAILURE(1)。大多数Unix shell都可检查进程的终止状态。若(a)调用这些函数时不带终止状态或(b)main函数执行了无返回值的return语句或(c) main函数未声明返回类型为整型则该进程的终止状态未定义。但若main函数的返回类型为整型且执行到最后一条语句时返回(隐式返回)则该进程的终止状态为0。exit系列函数是最简单直接的错误处理方式但程序出错终止时无法捕获异常信息。ISO C规定一个进程可以注册32个终止处理函数。这些函数可编写为自定义的清理代码将由exit()函数自动调用并可使用atexit()函数进行注册。#include stdlib.h
int atexit(void (*func)(void));该函数的参数是一个无参数无返回值的终止处理函数。exit()函数按注册的相反顺序调用这些函数。同一函数若注册多次则被调用多次。即使不调用exit函数程序退出时也会执行atexit注册的函数。通过结合exit()和atexit()函数可在程序出错终止时抛出异常信息。以除零错误为例double Division(double fDividend, double fDivisor)
{return fDividend/fDivisor;
}
void RaiseException1(void)
{printf(Exception is raised: \n);
}
void RaiseException2(void)
{printf(The divisor cannot be 0!\n);
}int main(void)
{double fDividend 0.0, fDivisor 0.0;printf(Enter the dividend: );scanf(%lf, fDividend);printf(Enter the divisor : );scanf(%lf, fDivisor);if(0 fDivisor){atexit(RaiseException2);atexit(RaiseException1);exit(EXIT_FAILURE);}printf(The quotient is %.2lf\n, Division(fDividend, fDivisor));return 0;
}执行结果为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)
#elseextern 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_VSNPRINTFvsnprintf(buf, MAXLINE, fmt, ap);
#elsevsprintf(buf, fmt, ap); /* This is not safe */
#endifn 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;
}*声明本文于网络整理版权归原作者所有如来源信息有误或侵犯权益请联系我们删除或授权事宜。戳“阅读原文”我们一起进步