前面的文章曾讨论,为了写出适应性更广的C语言程序,程序员考虑问题时应面面俱到。例如,在C语言程序中调用 open() 函数尝试打开文件时,应考虑到文件是否存在,当前程序是否有足够权限等情况。在打开文件失败时,需要做相应的错误处理,这样才能让程序的稳定性更强。
繁琐的判断
看到这里,相信有读者已经察觉到了,错误处理语句会让整个代码繁琐许多。例如:
int fd = open("filename", O_RDWR);
if(fd < 0){
printf("open file failed, %m\n");
return -1;
}
if(sizeof("hello") != write(fd, "hello", sizeof("hello")) ){
printf("write file failed, %m\n");
close(fd);
return -1;
}
close(fd);
上面这段C语言代码做了相应的错误处理,能够处理打开文件失败,和写数据失败的情况。但是多数情况下,我们的C语言程序都不太可能去操作一个不允许操作,或者不存在操作的文件,而写数据也很少出现失败的情况。事实上,如果不考虑极少出现的意外,上述C语言代码可以这么写:
int fd = open("filename", O_RDWR);
write(fd, "hello", sizeof("hello"));
close(fd);
显然,代码简洁多了。可是,虽然“意外”出现的几率比较低,我们仍然不能忽略它,否则一旦“意外”出现,程序崩溃退出还好,万一程序继续运行,造成不可预知的错误就麻烦了。所以,在编写C语言程序时,添加相应的错误判断和处理语句是必要的。
“投机取巧”
在C语言程序开发中,某个函数的执行状态常常使用返回码区分,也即 return 的值。究竟函数应该用什么样的返回值,决定什么执行状态,并没有强制标准。不过,对于 int 返回值类型的函数,大多数C语言程序员都爱使用 return 0; 表示函数执行成功,函数执行失败时,则返回一个负值(如 return -1;)。
请看下面这个函数:
int test(int val)
{
if(val % 99999 == 0)
return -1;
printf("val = %d\n", val);
return 0;
}
从上述C语言代码可以看出,test() 函数接收一个 int 型的参数 val,如果 val 为 99999 的倍数,则返回 -1(这里认为出错),否则打印出 val 的值并返回 0。
按照上面的讨论,开发C语言程序时应考虑各种“意外”,在调用 test() 函数时,需要相应的错误处理代码:
if(-1 == test(val)){
// do something
return -1;
}
在实际的C语言项目开发中,有时会遇到重复调用某个函数的情况,例如:
if(-1 == test(a)){
// do something
return -1;
}
if(-1 == test(b)){
// do something
return -1;
}
if(-1 == test(c)){
// do something
return -1;
}
...
if(-1 == test(m)){
// do something
return -1;
}
test() 函数出错的几率很小,但是为了程序的稳定性,仍然需要相应的错误处理代码。但是错误处理也让本来很简洁的代码段变得“啰嗦”,而且这些错误处理代码被执行的可能性微乎其微。
这里的test() 函数只是为了讨论主题提出的例子。读者应考虑实际情况,例如:没有程序员会调用 open() 打开一个不允许操作的文件,但是错误处理代码仍然是不可少的。
当然,重复的代码可以使用宏定义封装,这一点之前的文章已经讨论过,不再赘述了。值得说明的另外一个办法就是利用 test() 执行成功时返回值为 0 的特点,请看下面这段C语言代码:
int ret = 0;
ret += test(a);
ret += test(b);
ret += test(c);
...
ret += test(m);
if(0!=ret){
// do something
return -1;
}
可以看出,这样就只需写一处错误处理代码了,而且只要有一个 test() 执行失败,C语言程序就会执行它。这样是一个折中,在尽力维持代码简洁性的基础上,保留错误处理逻辑。
显然,只有 test() 的错误无需立刻处理时才能这么写。
定义错误代码行
一般来说,使用 ret+=test 折中方案只适合无需立刻处理错误的情况,此时所有的 test() 调用都被视为等价的操作。所以在错误处理代码中,常常不再考虑究竟是哪一个 test() 出错。不过,如果希望知道究竟是哪个或者哪几个函数 test() 出错,也是有办法的。
显然,应该从 test() 函数本身入手。最简单的办法就是在 test() 函数返回 -1 之前打印出输入的参数,修改后的 test() 的C语言代码如下:
int test(int val)
{
if(val % 99999 == 0){
printf("unexpected val: %d\n", val);
return -1;
}
printf("val = %d\n", val);
return 0;
}
这样就可以通过输入的参数 val 确定哪一个 test() 出错了。不过这种方法要求各个 test() 接收到的参数各不相同,否则就失效了。
幸好还有其他调试手段。如果能够知道 test() 函数的调用链(我之前的文章讨论过“调用链”这个概念),那C语言程序的出错路径也就显而易见了。所以,要定位究竟哪一个 test() 出错,只需在 test() 出错时将函数调用链打印出来就可以了。
打印函数调用链最直接的手段就是打印函数调用栈,不过获取函数调用栈涉及较深的操作系统知识,以后有机会再说。为了简便,这里使用 backtrace 函数族,相关的C语言原型如下:
#include <execinfo.h>
int backtrace(void **buffer, int size);
char **backtrace_symbols(void *const *buffer, int size);
buffer 是一个指针组,它们分别指向各个函数调用栈的起点,backtrace_symbols() 可以将 buffer 转换为相应的符号信息。
void print_trace()
{
int j, nptrs;
#define SIZE 100
void *buffer[100];
char **str
nptrs = backtrace(buffer, SIZE);
printf("backtrace() returned %d addresses\n", nptrs);
char **strings = backtrace_symbols(buffer, nptrs);
if (strings == NULL) {
return ;
}
for (j = 0; j < nptrs; j++)
printf("%s\n", strings[j]);
free(strings);
}
print_trace() 使用 backtrace 函数族,可以将函数调用情况打印出来。在 test() 函数执行失败时,可以调用 print_trace() 函数得到出错路径,也就可以进一步得到出错代码行了:
int test(int val)
{
if(val % 99999 == 0){
printf("unexpected val: %d\n", val);
print_trace();
return -1;
}
printf("val = %d\n", val);
return 0;
}
在 main() 函数中如下编写C语言代码:
int main()
{
int ret = 0;
ret += test(1);
ret += test(2);
ret += test(99999);
ret += test(4);
if(0!=ret){
// do something
return -1;
}
return 0;
}
编译并执行这段C语言程序,得到如下输出:
从上述打印信息,我们能够轻易知道 test() 函数出现了异常,并且此时是 main() 函数调用的 test()。查看C语言源代码,发现 main() 函数调用了多个 test(),那究竟是哪一个 test() 出错了呢?
根据打印出的“unexpected val: 99999”能够确定是第 39 行代码出错。也可以通过函数调用帧的地址确定出错代码行:
# addr2line -e a.out -af 0x400a6c
0x0000000000400a6c
main
t.c:39
已经很明显了,addr2line 命令直接将出错代码行(t.c:39)输出了。
某些嵌入式设备资源比较紧张,可能不支持原生的 backtrace 函数族,这时使用 __builtin_return_address
等编译器内置函数,也一样可以定位出错代码行,限于篇幅,以后有机会再说了。
小结
为了写出更加稳定的C语言程序,一般都需要错误处理代码的,不过很多情况下,错误处理代码被执行的可能性微乎其微。所以本节主要讨论了一种折中的方案,借助 ret+=fun() 的小技巧,可以少些不少代码。
另外,本节还讨论了如何定位错误代码行的方法。稍微思考下,应该能够发现 print_trace() 特别适合在C语言项目中比较偏底层的函数中使用,它能够尽力打印出一条尽力长的函数调用链。