原文:Exercise 20: Zed's Awesome Debug Macroshtml
译者:飞龙git
在C中有一个永恒的问题,它伴随了你很长时间,然而在这个练习我打算使用一系列我开发的宏来解决它。到如今为止你都不知道它们的强大之处,因此你必须使用它们,总有一天你会来找我说,“Zed,这些调试宏真是太伟大了,我应该把个人第一个孩子的出生归功于你,由于你治好了我十年的心脏病,而且打消了我数次想要自杀的念头。真是要谢谢你这样一个好人,这里有一百万美圆,和Leo Fender设计的Snakehead Telecaster电吉他的原型。”程序员
是的,它们的确很强大。github
几乎每一个编程语言中,错误处理都很是难。有些语言尽量试图避免错误这个概念,而另外一些语言发明了复杂了控制结构,好比异常来传递错误状态。固然的错误大可能是由于程序员嘉定错误不会发生,而且这一乐观的思想影响了他们所用和所创造的语言。数据库
C经过返回错误码或设置全局的errno
值来解决这些问题,而且你须要检查这些值。这种机制能够检查现存的复杂代码中,你执行的东西是否发生错误。当你编写更多的C代码时,你应该按照下列模式:编程
调用函数。安全
若是返回值出现错误(每次都必须检查)。服务器
清理建立的全部资源。app
打印出全部可能有帮助的错误信息。编程语言
这意味着对于每个函数调用(是的,每一个函数)你均可能须要多编写3~4行代码来确保它正常功能。这些还不包括清理你到目前建立的全部垃圾。若是你有10个不一样的结构体,3个方式。和一个数据库连接,当你发现错误时你应该写额外的14行。
以前这并非个问题,由于发生错误时,C程序会像你之前作的那样直接退出。你不须要清理任何东西,由于OS会为你自动去作。然而如今不少C程序须要持续运行数周、数月或者数年,而且须要优雅地处理来自于多种资源的错误。你并不能仅仅让你的服务器在首次运行就退出,你也不能让你写的库使使用它的程序退出。这很是糟糕。
其它语言经过异常来解决这个问题,可是这些问题也会在C中出现(其它语言也同样)。在C中你只可以返回一个值,可是异常是基于栈的返回系统,能够返回任意值。C语言中,尝试在栈上模拟异常很是困难,而且其它库也不会兼容。
我使用的解决方案是,使用一系列“调试宏”,它们在C中实现了基本的调试和错误处理系统。这个系统很是易于理解,兼容于每一个库,而且使C代码更加健壮和简洁。
它经过实现一系列转换来处理错误,任什么时候候发生了错误,你的函数都会跳到执行清理和返回错误代码的“error:”区域。你可使用check
宏来检查错误代码,打印错误信息,而后跳到清理区域。你也可使用一系列日志函数来打印出有用的调试信息。
我如今会向你展现你目前所见过的,最强大且卓越的代码的所有内容。
#ifndef __dbg_h__ #define __dbg_h__ #include <stdio.h> #include <errno.h> #include <string.h> #ifdef NDEBUG #define debug(M, ...) #else #define debug(M, ...) fprintf(stderr, "DEBUG %s:%d: " M "\n", __FILE__, __LINE__, ##__VA_ARGS__) #endif #define clean_errno() (errno == 0 ? "None" : strerror(errno)) #define log_err(M, ...) fprintf(stderr, "[ERROR] (%s:%d: errno: %s) " M "\n", __FILE__, __LINE__, clean_errno(), ##__VA_ARGS__) #define log_warn(M, ...) fprintf(stderr, "[WARN] (%s:%d: errno: %s) " M "\n", __FILE__, __LINE__, clean_errno(), ##__VA_ARGS__) #define log_info(M, ...) fprintf(stderr, "[INFO] (%s:%d) " M "\n", __FILE__, __LINE__, ##__VA_ARGS__) #define check(A, M, ...) if(!(A)) { log_err(M, ##__VA_ARGS__); errno=0; goto error; } #define sentinel(M, ...) { log_err(M, ##__VA_ARGS__); errno=0; goto error; } #define check_mem(A) check((A), "Out of memory.") #define check_debug(A, M, ...) if(!(A)) { debug(M, ##__VA_ARGS__); errno=0; goto error; } #endif
是的,这就是所有代码了,下面是它每一行所作的事情。
dbg.h:1-2
防止意外包含屡次的保护措施,你已经在上一个练习中见过了。
dbg.h:4-6
包含这些宏所需的函数。
dbg.h:8
#ifdef
的起始,它可让你从新编译程序来移除全部调试日志信息。
dbg.h:9
若是你定义了NDEBUG
以后编译,没有任何调试信息会输出。你能够看到#define debug()
被替换为空(右边没有任何东西)。
dbg.h:10
上面的#ifdef
所匹配的#else
。
dbg.h:11
用于替代的#define debug
,它将任何使用debug("format", arg1, arg2)
的地方替换成fprintf
对stderr
的调用。许多程序员并不知道,可是你的确能够建立列斯与printf
的可变参数宏。许多C编译器(其实是C预处理器)并不支持它,可是gcc能够作到。这里的魔法是使用##__VA_ARGS__
,意思是将剩余的全部额外参数放到这里。同时也要注意,使用了__FILE__
和__LINE__
来获取当前fine:line
用于调试信息。这会很是有帮助。
dbg.h:12
#ifdef
的结尾。
dbg.h:14
clean_errno
宏用于获取errno
的安全可读的版本。中间奇怪的语法是“三元运算符”,你会在后面学到它。
dbg.h:16-20
log_err
,log_warn
和log_info
宏用于为最终用户记录信息。它们相似于debug
但不能被编译。
dbg.h:22
到目前为止最棒的宏。check
会保证条件A
为真,不然会记录错误M
(带着log_err
的可变参数),以后跳到函数的error:
区域来执行清理。
dbg.h:24
第二个最棒的宏,sentinel
能够放在函数的任何不该该执行的地方,它会打印错误信息而且跳到error:
标签。你能够将它放到if-statements
或者switch-statements
的不应被执行的分支中,好比default
。
dbg.h:26
简写的check_mem
宏,用于确保指针有效,不然会报告“内存耗尽”的错误。
dbg.h:28
用于替代的check_debug
宏,它仍然会检查并处理错误,尤为是你并不想报告的广泛错误。它里面使用了debug
代替log_err
来报告错误,因此当你定义了NDEBUG
,它仍然会检查而且发生错误时跳出,可是不会打印消息了。
下面是一个例子,在一个小的程序中使用了dbg.h
的全部函数。这实际上并无作什么事情,知识想你演示了如何使用每一个宏。咱们将在接下来的全部程序中使用这些宏,全部要确保理解了如何使用它们。
#include "dbg.h" #include <stdlib.h> #include <stdio.h> void test_debug() { // notice you don't need the \n debug("I have Brown Hair."); // passing in arguments like printf debug("I am %d years old.", 37); } void test_log_err() { log_err("I believe everything is broken."); log_err("There are %d problems in %s.", 0, "space"); } void test_log_warn() { log_warn("You can safely ignore this."); log_warn("Maybe consider looking at: %s.", "/etc/passwd"); } void test_log_info() { log_info("Well I did something mundane."); log_info("It happened %f times today.", 1.3f); } int test_check(char *file_name) { FILE *input = NULL; char *block = NULL; block = malloc(100); check_mem(block); // should work input = fopen(file_name,"r"); check(input, "Failed to open %s.", file_name); free(block); fclose(input); return 0; error: if(block) free(block); if(input) fclose(input); return -1; } int test_sentinel(int code) { char *temp = malloc(100); check_mem(temp); switch(code) { case 1: log_info("It worked."); break; default: sentinel("I shouldn't run."); } free(temp); return 0; error: if(temp) free(temp); return -1; } int test_check_mem() { char *test = NULL; check_mem(test); free(test); return 1; error: return -1; } int test_check_debug() { int i = 0; check_debug(i != 0, "Oops, I was 0."); return 0; error: return -1; } int main(int argc, char *argv[]) { check(argc == 2, "Need an argument."); test_debug(); test_log_err(); test_log_warn(); test_log_info(); check(test_check("ex20.c") == 0, "failed with ex20.c"); check(test_check(argv[1]) == -1, "failed with argv"); check(test_sentinel(1) == 0, "test_sentinel failed."); check(test_sentinel(100) == -1, "test_sentinel failed."); check(test_check_mem() == -1, "test_check_mem failed."); check(test_check_debug() == -1, "test_check_debug failed."); return 0; error: return 1; }
要注意check
是如何使用的,而且当它为false
时会跳到error:
标签来执行清理。这一行读做“检查A是否为真,不为真就打印M并跳出”。
当你执行这段代码而且向第一个参数提供一些东西,你会看到:
$ make ex20 cc -Wall -g -DNDEBUG ex20.c -o ex20 $ ./ex20 test [ERROR] (ex20.c:16: errno: None) I believe everything is broken. [ERROR] (ex20.c:17: errno: None) There are 0 problems in space. [WARN] (ex20.c:22: errno: None) You can safely ignore this. [WARN] (ex20.c:23: errno: None) Maybe consider looking at: /etc/passwd. [INFO] (ex20.c:28) Well I did something mundane. [INFO] (ex20.c:29) It happened 1.300000 times today. [ERROR] (ex20.c:38: errno: No such file or directory) Failed to open test. [INFO] (ex20.c:57) It worked. [ERROR] (ex20.c:60: errno: None) I shouldn't run. [ERROR] (ex20.c:74: errno: None) Out of memory.
看到check
失败以后,它是如何打印具体的行号了吗?这会为接下来的调试工做节省时间。同时也观察errno
被设置时它如何打印错误信息。一样,这也能够节省你调试的时间。
如今我会想你简单介绍一些预处理器的工做原理,让你知道这些宏是如何工做的。我会拆分dbg.h
中阿最复杂的宏而且让你运行cpp
来让你观察它其实是如何工做的。
假设我有一个函数叫作dosomething()
,执行成功是返回0,发生错误时返回-1。每次我调用dosomething
的时候,我都要检查错误码,因此我将代码写成这样:
int rc = dosomething(); if(rc != 0) { fprintf(stderr, "There was an error: %s\n", strerror()); goto error; }
我想使用预处理器作的是,将这个if
语句封装为更可读而且便于记忆的一行代码。因而可使用这个check
来执行dbg.h
中的宏所作的事情:
int rc = dosomething(); check(rc == 0, "There was an error.");
这样更加简洁,而且刚好解释了所作的事情:检查函数是否正常工做,若是没有就报告错误。咱们须要一些特别的预处理器“技巧”来完成它,这些技巧使预处理器做为代码生成工具更加易用。再次看看check
和log_err
宏:
#define log_err(M, ...) fprintf(stderr, "[ERROR] (%s:%d: errno: %s) " M "\n", __FILE__, __LINE__, clean_errno(), ##__VA_ARGS__) #define check(A, M, ...) if(!(A)) { log_err(M, ##__VA_ARGS__); errno=0; goto error; }
第一个宏,log_err
更简单一些,只是将它本身替换为fprintf
对stderr
的调用。这个宏惟一的技巧性部分就是在log_err(M, ...)
的定义中使用...
。它所作的是让你向宏传入可变参数,从而传入fprintf
须要接收的参数。它们是如何注入fprintf
的呢?观察末尾的##__VA_ARGS__
,它告诉预处理器将...
所在位置的参数注入到fprintf
调用的相应位置。因而你能够像这样调用了:
log_err("Age: %d, name: %s", age, name);
age, name
参数就是...
所定义的部分,这些参数会被注入到fprintf
中,输出会变成:
fprintf(stderr, "[ERROR] (%s:%d: errno: %s) Age %d: name %d\n", __FILE__, __LINE__, clean_errno(), age, name);
看到末尾的age, name
了吗?这就是...
和##__VA_ARGS__
的工做机制,在调用其它变参宏(或者函数)的时候它会起做用。观察check
宏调用log_err
的方式,它也是用了...
和##__VA_ARGS__
。这就是传递整个printf
风格的格式字符串给check
的途径,它以后会传给log_err
,两者的机制都像printf
同样。
下一步是学习check
如何为错误检查构造if
语句,若是咱们剖析log_err
的用法,咱们会获得:
if(!(A)) { errno=0; goto error; }
它的意思是,若是A
为假,则重置errno
而且调用error
标签。check
宏会被上述if
语句·替换,因此若是咱们手动扩展check(rc == 0, "There was an error.")
,咱们会获得:
if(!(rc == 0)) { log_err("There was an error."); errno=0; goto error; }
在这两个宏的展开过程当中,你应该了解了预处理器会将宏替换为它的定义的扩展版本,而且递归地来执行这个步骤,扩展宏定义中的宏。预处理器是个递归的模板系统,就像我以前提到的那样。它的强大来源于使用参数化的代码来生成整个代码块,这使它成为便利的代码生成工具。
下面只剩一个问题了:为何不像die
同样使用函数呢?缘由是须要在错误处理时使用file:line
的数值和goto
操做。若是你在函数在内部执行这些,你不会获得错误真正出现位置的行号,而且goto
的实现也至关麻烦。
另外一个缘由是,若是你编写原始的if
语句,它看起来就像是你代码中的其它的if
语句,因此它看起来并不像一个错误检查。经过将if
语句包装成check
宏,就会使这一错误检查的逻辑更清晰,而不是主控制流的一部分。
最后,C预处理器提供了条件编译部分代码的功能,因此你能够编写只在构建程序的开发或调试版本时须要的代码。你能够看到这在dbg.h
中已经用到了,debug
宏的主体部分只被编译器用到。若是没有这个功能,你须要多出一个if
语句来检查是否为“调试模式”,也浪费了CPU资源来进行没有必要的检查。
将#define NDEBUG
放在文件顶端来消除全部调试信息。
撤销上面添加的一行,并在MakeFile
顶端将-D NDEBUG
添加到CFLAGS
,以后从新编译来达到一样效果。
修改日志宏,使之包含函数名称和file:line
。