# 练习20:Zed的强大的调试宏
> 原文:[Exercise 20: Zed's Awesome Debug Macros](http://c.learncodethehardway.org/book/ex20.html)
> 译者:[飞龙](https://github.com/wizardforcel)
在C中有一个永恒的问题,它伴随了你很长时间,然而在这个练习我打算使用一系列我开发的宏来解决它。到现在为止你都不知道它们的强大之处,所以你必须使用它们,总有一天你会来找我说,“Zed,这些调试宏真是太伟大了,我应该把我的第一个孩子的出生归功于你,因为你治好了我十年的心脏病,并且打消了我数次想要自杀的念头。真是要谢谢你这样一个好人,这里有一百万美元,和Leo Fender设计的Snakehead Telecaster电吉他的原型。”
是的,它们的确很强大。
## C的错误处理问题
几乎每个编程语言中,错误处理都非常难。有些语言尽可能试图避免错误这个概念,而另一些语言发明了复杂了控制结构,比如异常来传递错误状态。当然的错误大多是因为程序员嘉定错误不会发生,并且这一乐观的思想影响了他们所用和所创造的语言。
C通过返回错误码或设置全局的`errno`值来解决这些问题,并且你需要检查这些值。这种机制可以检查现存的复杂代码中,你执行的东西是否发生错误。当你编写更多的C代码时,你应该按照下列模式:
+ 调用函数。
+ 如果返回值出现错误(每次都必须检查)。
+ 清理创建的所有资源。
+ 打印出所有可能有帮助的错误信息。
这意味着对于每一个函数调用(是的,每个函数)你都可能需要多编写3~4行代码来确保它正常功能。这些还不包括清理你到目前创建的所有垃圾。如果你有10个不同的结构体,3个方式。和一个数据库链接,当你发现错误时你应该写额外的14行。
之前这并不是个问题,因为发生错误时,C程序会像你以前做的那样直接退出。你不需要清理任何东西,因为OS会为你自动去做。然而现在很多C程序需要持续运行数周、数月或者数年,并且需要优雅地处理来自于多种资源的错误。你并不能仅仅让你的服务器在首次运行就退出,你也不能让你写的库使使用它的程序退出。这非常糟糕。
其它语言通过异常来解决这个问题,但是这些问题也会在C中出现(其它语言也一样)。在C中你只能够返回一个值,但是异常是基于栈的返回系统,可以返回任意值。C语言中,尝试在栈上模拟异常非常困难,并且其它库也不会兼容。
## 调试宏
我使用的解决方案是,使用一系列“调试宏”,它们在C中实现了基本的调试和错误处理系统。这个系统非常易于理解,兼容于每个库,并且使C代码更加健壮和简洁。
它通过实现一系列转换来处理错误,任何时候发生了错误,你的函数都会跳到执行清理和返回错误代码的“error:”区域。你可以使用`check`宏来检查错误代码,打印错误信息,然后跳到清理区域。你也可以使用一系列日志函数来打印出有用的调试信息。
我现在会向你展示你目前所见过的,最强大且卓越的代码的全部内容。
```c
#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
下面是一个例子,在一个小的程序中使用了`dbg.h`的所有函数。这实际上并没有做什么事情,知识想你演示了如何使用每个宏。我们将在接下来的所有程序中使用这些宏,所有要确保理解了如何使用它们。
```c
#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并跳出”。
## 你会看到什么
当你执行这段代码并且向第一个参数提供一些东西,你会看到:
```sh
$ 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`被设置时它如何打印错误信息。同样,这也可以节省你调试的时间。
## C预处理器如果扩展宏
现在我会想你简单介绍一些预处理器的工作原理,让你知道这些宏是如何工作的。我会拆分`dbg.h`中阿最复杂的宏并且让你运行`cpp`来让你观察它实际上是如何工作的。
假设我有一个函数叫做`dosomething()`,执行成功是返回0,发生错误时返回-1。每次我调用`dosomething`的时候,我都要检查错误码,所以我将代码写成这样:
```c
int rc = dosomething();
if(rc != 0) {
fprintf(stderr, "There was an error: %s\n", strerror());
goto error;
}
```
我想使用预处理器做的是,将这个`if`语句封装为更可读并且便于记忆的一行代码。于是可以使用这个`check`来执行`dbg.h`中的宏所做的事情:
```c
int rc = dosomething();
check(rc == 0, "There was an error.");
```
这样更加简洁,并且恰好解释了所做的事情:检查函数是否正常工作,如果没有就报告错误。我们需要一些特别的预处理器“技巧”来完成它,这些技巧使预处理器作为代码生成工具更加易用。再次看看`check`和`log_err`宏:
```c
#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`调用的相应位置。于是你可以像这样调用了:
```c
log_err("Age: %d, name: %s", age, name);
```
`age, name`参数就是`...`所定义的部分,这些参数会被注入到`fprintf`中,输出会变成:
```c
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`的用法,我们会得到:
```c
if(!(A)) { errno=0; goto error; }
```
它的意思是,如果`A`为假,则重置`errno`并且调用`error`标签。`check`宏会被上述`if`语句·替换,所以如果我们手动扩展`check(rc == 0, "There was an error.")`,我们会得到:
```c
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`。
- 笨办法学C 中文版
- 前言
- 导言:C的笛卡尔之梦
- 练习0:准备
- 练习1:启用编译器
- 练习2:用Make来代替Python
- 练习3:格式化输出
- 练习4:Valgrind 介绍
- 练习5:一个C程序的结构
- 练习6:变量类型
- 练习7:更多变量和一些算术
- 练习8:大小和数组
- 练习9:数组和字符串
- 练习10:字符串数组和循环
- 练习11:While循环和布尔表达式
- 练习12:If,Else If,Else
- 练习13:Switch语句
- 练习14:编写并使用函数
- 练习15:指针,可怕的指针
- 练习16:结构体和指向它们的指针
- 练习17:堆和栈的内存分配
- 练习18:函数指针
- 练习19:一个简单的对象系统
- 练习20:Zed的强大的调试宏
- 练习21:高级数据类型和控制结构
- 练习22:栈、作用域和全局
- 练习23:认识达夫设备
- 练习24:输入输出和文件
- 练习25:变参函数
- 练习26:编写第一个真正的程序
- 练习27:创造性和防御性编程
- 练习28:Makefile 进阶
- 练习29:库和链接
- 练习30:自动化测试
- 练习31:代码调试
- 练习32:双向链表
- 练习33:链表算法
- 练习34:动态数组
- 练习35:排序和搜索
- 练习36:更安全的字符串
- 练习37:哈希表
- 练习38:哈希算法
- 练习39:字符串算法
- 练习40:二叉搜索树
- 练习41:将 Cachegrind 和 Callgrind 用于性能调优
- 练习42:栈和队列
- 练习43:一个简单的统计引擎
- 练习44:环形缓冲区
- 练习45:一个简单的TCP/IP客户端
- 练习46:三叉搜索树
- 练习47:一个快速的URL路由
- 后记:“解构 K&R C” 已死