Menu

C 语言宏:#define、带参数的宏和括号规则

用 #define 定义对象式宏和函数式宏:为什么每个参数和整个宏体都要加括号,多次求值陷阱,用 do-while(0) 写多行宏,以及什么时候函数或 const 是更好的选择。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

宏是一个名字,预处理器会在编译器运行之前把它替换成一段文本。这就是全部机制,本页的每条规则都由此而来:宏没有类型,不遵守作用域,不会对参数求值,也不知道什么是函数调用。它们只是复制文本。

谨慎使用时,宏是不可或缺的:可移植性开关、能记录行号的断言宏、可以用作数组大小的编译期常量。随意使用时,它会产生一些看起来不可能出现的 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()) 会读走两个字节。

有两种防御办法,按推荐顺序:

  1. 使用真正的函数。如果需要支持多种类型,就为每种类型各写一个,或者在头文件中使用 static inline 函数。
  2. 如果必须用宏,就醒目地注明参数会被多次求值,并确保调用处不带副作用。

多行宏和 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) 是一条语句,后面可以接分号,宏体恰好执行一次。

Coddy 编程语言插图

和 Coddy 一起学编程

开始