11

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

 3 years ago
source link: https://blog.popkx.com/c%E8%AF%AD%E8%A8%80%E9%99%B7%E9%98%B1%E4%B8%8E%E6%8A%80%E5%B7%A7%E7%AC%AC30%E8%8A%82-%E5%BE%88%E5%A4%9A%E7%A8%8B%E5%BA%8F%E5%91%98%E4%B8%8D%E7%9F%A5%E9%81%93%E7%9A%84%E5%B0%8F%E6%8A%80/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

前面的文章曾讨论,为了写出适应性更广的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;
}
84c31c397b870da49a0cee1bba98cd41.png

编译并执行这段C语言程序,得到如下输出:
dbbe744072176244f838e11a0c73f6c2.png

从上述打印信息,我们能够轻易知道 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语言项目中比较偏底层的函数中使用,它能够尽力打印出一条尽力长的函数调用链。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK