C语言陷阱与技巧第30节,很多程序员不知道的小技巧,能减少代码量

前面的文章曾讨论,为了写出适应性更广的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语言项目中比较偏底层的函数中使用,它能够尽力打印出一条尽力长的函数调用链。

阅读更多:   C语言
添加新评论

icon_redface.gificon_idea.gificon_cool.gif2016kuk.gificon_mrgreen.gif2016shuai.gif2016tp.gif2016db.gif2016ch.gificon_razz.gif2016zj.gificon_sad.gificon_cry.gif2016zhh.gificon_question.gif2016jk.gif2016bs.gificon_lol.gif2016qiao.gificon_surprised.gif2016fendou.gif2016ll.gif