宏是一个名字,预处理器会在编译器运行之前把它替换成一段文本。这就是全部机制,本页的每条规则都由此而来:宏没有类型,不遵守作用域,不会对参数求值,也不知道什么是函数调用。它们只是复制文本。
谨慎使用时,宏是不可或缺的:可移植性开关、能记录行号的断言宏、可以用作数组大小的编译期常量。随意使用时,它会产生一些看起来不可能出现的 bug,直到你把文件展开才明白原因。
对象式宏
最简单的形式是定义一个代表某个值的名字:
按照惯例,这些名字使用 UPPER_SNAKE_CASE,这样读代码的人一眼就能看出某个符号是宏而不是变量。这个惯例在这里比平时更重要,因为宏会忽略作用域:写在函数内部的 #define 对文件的其余部分仍然有效,它还会毫不犹豫地改写另一个函数中同名的变量。
#undef NAME 会删除一个定义,之后可以重新定义这个名字,而不会收到重复定义的警告。
函数式宏
在名字后面紧跟一个参数列表(左圆括号前不能有空格),宏就可以接收参数:
空格规则是真实存在的:#define SQUARE (x) ((x)*(x)) 定义的是一个名为 SQUARE 的对象式宏,它的替换文本以 (x) 开头。随之而来的编译错误不会提到空格的问题。
括号规则
括号规则有两条,它们之所以存在,是因为宏体会被粘贴进一个你在写宏时看不到的表达式中。
规则 1:给每个参数加括号。 不加括号的话,如果参数本身是一个表达式,就会被运算符优先级拆散:
#define SQUARE_BAD(x) x * x
SQUARE_BAD(2 + 3) /* becomes 2 + 3 * 2 + 3 == 11, not 25 */
规则 2:给整个宏体加括号。 不加括号的话,周围的表达式会把宏的结果拆散:
#define DOUBLE_BAD(x) (x) + (x)
10 / DOUBLE_BAD(5) /* becomes 10 / (5) + (5) == 7, not 1 */
把两个错误版本和修正后的版本放在一起运行:
两个错误版本都能编译通过,没有任何警告,却算出了错误的数字。要养成的习惯很机械:先给每个参数加括号,再给结果加括号。((a) > (b) ? (a) : (b)) 看起来很杂乱,但这些杂乱正是关键所在。
多次求值陷阱
括号救不了第二种危险。宏会把参数文本粘贴到每个出现该参数的位置,所以带副作用的参数会被执行不止一次:
这个宏展开后是 ((i++) > (j) ? (i++) : (j))。比较时 i 自增到 6,比较的是 5 和 3;真分支又让 i 自增到 7,并得到 6。所以 m 是 6,i 是 7,两者都不是“i 和 j 中较大的那个”应有的结果。如果是函数 max(i++, j),结果就是正确的,因为参数在调用前只求值一次。
任何带副作用的东西都会掉进同样的陷阱:SQUARE(rand()) 会调用两次 rand(),把两个不同的数相乘。CHECK(read_byte()) 会读走两个字节。
有两种防御办法,按推荐顺序:
- 使用真正的函数。如果需要支持多种类型,就为每种类型各写一个,或者在头文件中使用
static inline函数。 - 如果必须用宏,就醒目地注明参数会被多次求值,并确保调用处不带副作用。
多行宏和 do-while(0)
如果宏体除最后一行外每行都以反斜杠结尾,宏体就可以跨越多行。简单的写法看起来没问题:
#define LOG_TWICE(msg) \
printf("%s\n", msg); \
printf("%s\n", msg)
然后它会在最关键的地方悄悄出错:
if (verbose)
LOG_TWICE("hello");
else
printf("quiet\n");
替换之后,if 只管得到第一个 printf,第二个无条件执行,而 else 再也找不到与之配对的 if,于是产生一个编译错误,错误信息却指不到有用的地方。用普通的花括号包住宏体也好不到哪里去:LOG_TWICE("hello") 后面的 ; 会成为一个空语句,结束这个 if,else 又一次出错。
可行的惯用写法是 do { ... } while (0):
do { ... } while (0) 是一条语句,它的循环体恰好执行一次,而且后面必须跟一个分号,所以调用处读起来就像普通的函数调用,在任何控制流中的行为也和函数调用一样。注意反斜杠:反斜杠后面多出一个空格,就会中断续行,产生令人费解的错误。
预定义宏和断言模式
预处理器自己定义了几个宏,正因为有它们,有些东西根本无法写成函数:
__FILE__ 和 __LINE__ 在它们被写下的位置展开,所以把它们放进宏里,就能记录调用者的位置。函数做不到这一点:在函数内部,它们总是报告这个日志函数自己的文件和行号。标准的 assert 宏正是用这种方式报告失败表达式的位置。
其他有用的预定义名字:__DATE__、__TIME__ 和 __STDC_VERSION__(例如 C17 对应 201710L)。
两个值得了解的运算符
在宏体中,# 会把参数变成字符串字面量(字符串化),## 会把记号连接在一起(记号粘贴)。
SHOW(width * height) 会变成 printf("width * height" " = %d\n", (width * height));,相邻的字符串字面量会被编译器合并,所以一次调用就能同时打印出表达式文本和它的值。这是一个值得记住的调试技巧。
## 用得更少,主要出现在生成代码的宏中:#define MAKE_VAR(n) int var_##n 会把 MAKE_VAR(3) 变成 int var_3。要谨慎使用;由预处理器拼出来的标识符无法按名字搜索,会让代码难以浏览。
宏、函数与 const
选用能完成任务的最弱的工具:
const变量:const double PI = 3.14159;有类型,遵守作用域,能在调试器中看到,也不会被不相关的头文件重新定义。运行时使用的值优先用它。enum:enum { MAX_USERS = 100 };提供一个有名字的整数常量,可以用在 C 要求编译期常量的地方,并且有类型和作用域。适合表示整数上限。static inline函数:有类型检查,每个参数恰好求值一次,现代编译器也会像宏一样把它内联。几乎所有函数式宏都应该用它来替代。- 宏:需要
__LINE__时,需要在类型系统起作用之前就能工作时(旧代码中的数组大小),用条件编译切换代码时,或者生成重复代码时。
宏仍然无可替代的一个地方是计算数组大小,因为结果必须是编译期常量:
这个宏也有一个众所周知的注意事项:它只对真正的数组才正确。如果传入指针(数组传给函数后就会变成指针),sizeof 量到的是指针的大小,给出一个错误的答案,而且不会有任何提示。
调试宏
宏出问题时,不要盯着它看,把它展开:
gcc -E program.c | tail -30
替换后的文本会立刻告诉你,问题是少了括号、重复求值,还是名字和别的东西冲突了。编译时也要加上 -Wall -Wextra:GCC 和 clang 会附加 "in expansion of macro" 提示,把报错的那一行和宏的定义联系起来。
接下来是头文件:#define 和 #include 配合使用,让一个项目可以跨越多个源文件,而不必把同样的声明粘贴两次。
常见问题
C 语言中的宏是什么?
宏是一段有名字的文本,预处理器会在编译前把它替换到源代码中。#define MAX 100 会让之后每个 MAX 都变成 100;#define SQUARE(x) ((x) * (x)) 接收参数,并把参数代入一个模板。宏没有类型,也不遵守作用域规则,它只是文本替换。
为什么 C 语言的宏需要这么多括号?
因为宏体会被粘贴到周围的代码中,然后作为一个整体解析。#define SQUARE(x) x * x 会把 SQUARE(2 + 3) 变成 2 + 3 * 2 + 3,结果是 11 而不是 25。给每个参数和整个宏体都加上括号,写成 ((x) * (x)),无论周围是什么,替换后的文本都会按你的意图分组。
C 语言中宏和函数有什么区别?
函数只编译一次,会做类型检查,每个参数恰好求值一次。宏在每次使用的地方都会被粘贴一遍,什么也不检查,还可能把一个参数求值多次,所以 MAX(i++, j) 可能让 i 自增两次。优先使用函数(常量用 const 或 enum);宏用于函数做不到的事情,比如获取 __LINE__ 或生成代码。
为什么多行宏要用 do { ... } while (0) 包起来?
为了让它表现得像一条语句。如果宏体是一个裸的 { ... },在 if/else 中后面跟上分号就会出错(分号让 if 提前结束);如果宏体只是几条语句的序列,在没有花括号的 if 中也会出错。do { ... } while (0) 是一条语句,后面可以接分号,宏体恰好执行一次。