一 ?错误概念
1.1 错误分类
? ? ?从严重性而言,程序错误可分为致命性和非致命性两类。对于致命性错误,无法执行恢复动作,最多只能在用户屏幕上打印出错消息或将其写入日志文件,然后终止程序;而对于非致命性错误,多数本质上是暂时的(如资源短缺),一般恢复动作是延迟一些时间后再次尝试。
?
? ? ?从交互性而言,程序错误可分为用户错误和内部错误两类。用户错误呈现给用户,通常指明用户操作上的错误;而程序内部错误呈现给程序员(可能携带用户不可接触的数据细节),用于查错和排障。
?
? ? ?应用
程序开发者可决定恢复哪些错误以及如何恢复。例如,若磁盘已满,可考虑删除非必需或已过期的数据;若网络连接失败,可考虑短时间延迟后重建连接。选择合理的错误恢复策略,可避免应用程序的异常终止,从而改善其健壮性。
?
1.2 处理步骤
? ? ?错误处理即处理程序运行时出现的任何意外或异常情况。典型的错误处理包含五个步骤:
?
? ? ?1) 程序执行时发生软件错误。该错误可能产生于被底层驱动或内核映射为软件错误的硬件响应事件(如除零)。
?
? ? ?2) 以一个错误指示符(如整数或结构体)记录错误的原因及相关信息。?
?
? ? ?3) 程序检测该错误(读取错误指示符,或由其主动上报);?
?
? ? ?4) 程序决定如何处理错误(忽略、部分处理或完全处理);?
?
? ? ?5) 恢复或终止程序的执行。?
?
?
复制代码
?1 int func()
?2 {
?3 ? ? int bIsErrOccur = 0;
?4 ? ? //do something that might invoke errors
?5 ? ? if(bIsErrOccur) ?//Stage 1: error occurred
?6 ? ? ? ? return -1; ? //Stage 2: generate error indicator
?7 ? ? //...
?8 ? ? return 0;
?9 }
10 ??
11 int main(void)
12 {
13 ? ? if(func() != 0) ?//Stage 3: detect error
14 ? ? {
15 ? ? ? ? //Stage 4: handle error
16 ? ? }
17 ? ? //Stage 5: recover or abort
18 ? ? return 0;
19 }
复制代码
? ? ?调用者可能希望函数返回成功时表示完全成功,失败时程序恢复到调用前的状态(但被调函数很难保证这点)。
?
?
?
二 ?错误传递
2.1 返回值和回传参数
? ? ?
C语言通常使用返回值来标志函数是否执行成功,调用者通过if等语句检查该返回值以判断函数执行情况。常见的几种调用形式如下:
?
复制代码
1 if((p = malloc(100)) == NULL)
2 ? ?//...
3 ??
4 if((c = getchar()) == EOF)
5 ? ?//...
6 ??
7 if((ticks = clock()) < 0)
8 ? ?//...
复制代码
? ? ?Unix系统调用级函数(和一些老的Posix函数)的返回值有时既包括错误代码也包括有用结果。因此,上述调用形式可在同一条语句中接收返回值并检查错误(当执行成功时返回合法的数据值)。
?
? ? ?返回值方式的好处是简便和高效,但仍存在较多问题:
?
? ? ?1) 代码可读性降低
?
没有返回值的函数是不可靠的。但若每个函数都具有返回值,为保持程序健壮性,就必须对每个函数进行正确性验证,即调用时检查其返回值。这样,代码中很大一部分可能花费在错误处理上,且排错代码和正常流程代码搅在一起,比较混乱。
?
? ? ?2) 质量降级
?
条件语句相比其他类型的语句潜藏更多的错误。不必要的条件语句会增加排障和白盒测试的工作量。
?
? ? ?3) 信息有限
?
? ? ?通过返回值只能返回一个值,因此一般只能简单地标志成功或失败,而无法作为获知具体错误信息的手段。通过按位编码可变通地返回多个值,但并不常用。字符串处理函数可参考IntToAscii()来返回具体的错误原因,并支持链式表达:
?
复制代码
?1 char *IntToAscii(int dwVal, char *pszRes, int dwRadix)
?2 {
?3 ? ? if(NULL == pszRes)
?4 ? ? ? ? return "Arg2Null";
?5 ? ??
?6 ? ? if((dwRadix < 2) || (dwRadix > 36))
?7 ? ? ? ? return "Arg3OutOfRange";
?8?
?9 ? ? //...
10 ? ? return pszRes;
11 }
复制代码
? ? ?4) 定义冲突
?
? ? ?不同函数在成功和失败时返回值的取值规则可能不同。例如,Unix系统调用级函数返回0代表成功,-1代表失败;新的Posix函数返回0代表成功,非0代表失败;标准C库中isxxx函数返回1表示成功,0表示失败。
?
? ? ?5) 无约束性
?
? ? ?调用者可以忽略和丢弃返回值。未检查和处理返回值时,程序仍然能够运行,但结果不可预知。
?
? ? ?新的Posix函数返回值只携带状态和异常信息,并通过参数列表中的指针回传有用的结果。 回传参数绑定到相应的实参上,因此调用者不可能完全忽略它们。通过回传参数(如结构体指针)可返回多个值,也可携带更多的信息。
?
? ? ?综合返回值和回传参数的优点,可对Get类函数采用返回值(含有用结果)方式,而对Set类函数采用返回值+回传参数方式。对于纯粹的返回值,可按需提供如下解析接口:
?
复制代码
?1 typedef enum{
?2 ? ? S_OK, ? ? ? ? ? ? ? ? ? //成功
?3 ? ? S_ERROR, ? ? ? ? ? ? ? ?//失败(原因未明确),通用状态
?4?
?5 ? ? S_NULL_POINTER, ? ? ? ? //入参指针为NULL
?6 ? ? S_ILLEGAL_PARAM, ? ? ? ?//参数值非法,通用
?7 ? ? S_OUT_OF_RANGE, ? ? ? ? //参数值越限
?8 ? ? S_MAX_STATUS ? ? ? ? ? ?//不可作为返回值状态,仅作枚举最值使用
?9 }FUNC_STATUS;
10?
11 #define RC_NAME(eRetCode) \
12 ? ? ((eRetCode) == S_OK ? ? ? ? ? ? ? ? ? ? ? ?"Success" ? ? ? ? ? ? : \
13 ? ? ((eRetCode) == S_ERROR ? ? ? ? ? ? ? ?? ? ?"Failure" ? ? ? ? ? ? : \
14 ? ? ((eRetCode) == S_NULL_POINTER ? ? ? ? ? ? ?"NullPointer" ? ? ? ? : \
15 ? ? ((eRetCode) == S_ILLEGAL_PARAM ? ? ? ?? ? ?"IllegalParas" ? ? ? ?: \
16 ? ? ((eRetCode) == S_OUT_OF_RANGE ? ? ?