预处理¶
预处理发生在语法分析之前.
预处理不识别类型, 作用域和花括号, 只做文本操作:
-
把续行接上
-
删掉注释
-
展开宏
-
按条件丢掉整段源码
-
再把别的文件贴进来
宏从定义处一直生效到 #undef 或文件结束, 写在函数里面也一样.
观察预处理展开结果可以使用 gcc -E 或 cpp, 这里的cpp指预处理器(C Preprocessor). 这一步不检查 C 语法, 源文件暂时写不完整也没关系. gcc -E 的输出里会夹着 # 行号 "文件名" 这种标记, 那是给后面的编译器看的.
约定¶
-
一条预处理指示只占一个逻辑行. 写不下一行就在行尾加
\,\和换行之间不能有空格或 Tab. C 语句换行不用\, 因为换行到语法分析时只是空白. -
函数式宏的每个形参都加括号, 整个替换列表再加一层. 宏名和
(之间不能有空白, 有空白就不再是函数式宏. -
实参有副作用, 或者一次调用里会被算很多次时, 写成函数. 函数式宏会把实参原样贴进每个形参出现的地方.
-
#和##的操作数不先做宏展开. 要把__LINE__变成字符串, 得先套一层宏, 让行号先展开成数字. -
#if看不见 C 的变量和enum常量. 展开之后还剩下的名字换成0. 这里的整数按intmax_t/uintmax_t的宽度算, 和运行时同一种写法可能结果不同.
翻译阶段¶
从源文件到可执行文件, 标准分成 8 个翻译阶段. 教材也写了 8 步, 但切分不同: 它把"去注释"和"切 token"拆开, 标准合在第 3 阶段. 下面按标准的前 6 个阶段写, 顺序和教材一致. 第 7 阶段做语法分析, 第 8 阶段链接.
-
源字符映射成内部字符集. C23 之前还会把三字符组换成单字符, 例如
??=换成#. C23 删掉了三字符组. -
行尾的
\紧挨换行时, 把下一行接上来. -
注释换成一个空格, 再把逻辑行切成预处理 token 和空白. 标识符, 常量, 字符串, 运算符都算 token.
-
执行预处理指示.
#include把文件插进来, 插进来的内容再走前面几步. 这一步做宏展开. -
字符常量和字符串里的转义序列换成对应字节, 例如
\n换成0x0a. -
相邻的字符串字面量拼成一个.
之后空白丢掉, token 交给编译器. 指示本身必须在第 4 步就认出来, 所以不能靠宏展开出一条 #define.
Example
以如下代码为例:
#define STR "hello, "\
"world"
printf(STR);
第 2 步接成一行 #define STR "hello, " "world". 展开后是两个挨着的字符串, 在第 6 步拼成 "hello, world".
宏定义¶
名字习惯全大写加下划线, 和常量的命名一样.
对象式宏¶
#define N 20 这种, 名字出现的地方换成后面的替换列表.
同一个名字再 #define 一次, 替换列表必须 token 对 token 相同. 空白可多可少, 有没有空白算不同:
#define OBJ_LIKE (1 - 1)
#define OBJ_LIKE (1 - 1) /* 可以, 空白多少无所谓 */
#define OBJ_LIKE (1-1) /* 不可以, 少了空格 */
要改定义, 先 #undef. #undef 一个本来就没有的名字不算错.
函数式宏¶
#define MAX(a, b) ((a) > (b) ? (a) : (b)). 调用写成 MAX(i & 0x0f, j & 0x0f), 实参贴到形参的位置.
和函数调用的差别:
-
参数没有类型, 预处理器不做类型检查.
-
每次调用都把替换列表贴进去, 没有
call, 也没有单独的栈帧. 目标文件更大. -
括号不能省. 省掉形参两边的括号,
MAX(i & 0x0f, j & 0x0f)会按>比&更紧来拆. 省掉最外层括号,MAX(i, j) + 1会变成i > j ? i : j + 1, 因为+比?:更紧. -
函数先把实参求完值再传入, 副作用只发生一次. 宏把实参贴进每个使用点,
MAX(++a, ++b)展开后自增可能发生两次. -
实参会被重复计算. 下面的
MAX若是宏, 展开之后条件运算符只算被选中的那一支, 但比较式里已经算过一次max(n - 1). 数组最大值在a[0], 每一层比较都失败, 被选中的分支里再算一次, 于是每层递归两次, 变成 \(\Theta(2^n)\). 换成真正的函数只遍历一遍, 是 \(\Theta(n)\).
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int a[] = { 9, 3, 5, 2, 1, 0, 8, 7, 6, 4 };
int max(int n)
{
return n == 0 ? a[0] : MAX(a[n], max(n - 1));
}
多条语句的宏用 do { ... } while (0) 包起来, 这样调用处可以像函数一样写分号, 放在 if / else 里也还是一条语句.
#define device_init_wakeup(dev, val) \
do { \
device_can_wakeup(dev) = !!(val); \
device_set_wakeup_enable(dev, val); \
} while (0)
不用这个包装, 只有第一条语句进 if. 只加大括号, 调用处那个分号会把 if 结束掉, 后面的 else 对不上.
字符串化与粘贴¶
# 只能用在函数式宏里, 后面跟一个形参, 把实参收成一个字符串. 连续空白收成一个空格. 实参里的 " 和 \ 会转义, 避免把生成的字符串截断.
#define STR(s) #s
STR(hello world) /* "hello world" */
## 把左右两个预处理 token 粘成一个. 对象式宏也能用. 粘完必须还是一个合法的预处理 token, 也不能出现在替换列表的两头.
#define CONCAT(a, b) a ## b
CONCAT(con, cat) /* concat */
#define HASH_HASH # ## #
/* 中间的 ## 是运算符, 两边的 # 被粘成 ## */
/* 写成 #### 会被切成两个 ##, 没有左右 token 可粘 */
可变参数¶
参数列表里的 ... 用 __VA_ARGS__ 接住, 和 printf 的可变参数同一套写法. 函数调用不能少传参数, 函数式宏可以传空参数, 但逗号的个数仍要对上.
#define report(test, ...) \
((test) ? printf(#test) : printf(__VA_ARGS__))
report(x > y, "x is %d but y is %d", x, y);
## 放在 __VA_ARGS__ 前面, 并且可变参数为空时把前面的逗号吃掉, 这是 gcc 的扩展:
#define DEBUGP(format, ...) printk(format, ## __VA_ARGS__)
DEBUGP("info"); /* printk("info"), 不是 printk("info",) */
C23 的标准写法是 __VA_OPT__:
#define DEBUGP(format, ...) printk(format __VA_OPT__(,) __VA_ARGS__)
展开顺序¶
-
充当
#或##操作数的实参, 按写下来的 token 使用, 不先展开. -
其余实参先充分展开, 再替换进替换列表.
-
替换完再扫一遍, 把新出现的宏展开.
-
正在展开的那个宏, 扫回来时不再展开自己, 所以
#define a a[0]不会无限递归.
#define sh(x) printf("n" #x "=%d, or %d\n", n ## x, alt[x])
#define sub_z 26
sh(sub_z);
#x 得到 "sub_z", n ## x 得到 nsub_z, 这两个都不先展开 sub_z. alt[x] 里的 x 会先把 sub_z 展开成 26. 替换完再扫一遍, 如果 nsub_z 或 alt 自己也是宏, 这一遍才会展开它们.
#define x 2
#define f(a) f(x * (a))
f(0); /* f(2 * (0)), 里面的 f 不再展开 */
条件编译¶
头文件守卫让同一个头被间接包含多次时, 正文只留第一次:
#ifndef HEADER_FILENAME
#define HEADER_FILENAME
/* body of header */
#endif
#pragma once 很多编译器都认, 但不是 C 标准. 同一份文件经过不同路径 (例如符号链接) 被包含时, 它也可能认成两个文件. 要可移植就写守卫.
条件指示有 #if, #ifdef, #ifndef, #elif, #else, #endif. C23 增加了 #elifdef 和 #elifndef. 可以嵌套. # 前面允许有空白, 习惯上仍顶格写. 嵌套深时在 #endif 后用注释标明它结束的是哪一个条件, 例如 #endif /* CONFIG_NETDEVICES */.
#if MACHINE == 68000
int x;
#elif MACHINE == 8086
long x;
#else
#error UNKNOWN TARGET MACHINE
#endif
MACHINE 可以写在源文件里, 也可以写在一个所有源文件都包含的配置头里, 还可以在编译命令上给: gcc -c -DMACHINE=8086 main.c. 多文件工程把 -D 放进 Makefile, 就不用每个文件手改. Linux 内核的 make menuconfig 走的是配置头这一条: 选项写成 autoconf.h 里的 #define CONFIG_FOO 1, 源文件用 #ifdef CONFIG_FOO 决定这段代码进不进内核.
#if 后面的表达式这样求值:
#define VERSION 2
#if defined x || y || VERSION < 3
-
先处理
defined. 宏x有定义就是1, 否则是0. 单独写时,#if defined x等于#ifdef x,#if !defined x等于#ifndef x. -
再展开有定义的宏, 这里
VERSION换成2. -
剩下的标识符换成
0. 就算前面有一个 C 变量叫y, 这里也是0.sizeof这个关键字同样换成0, 所以#if里不能问类型的大小, 也看不见enum常量. -
按整数常量表达式求值. 上面得到
0 || 0 || 2 < 3, 结果非 0, 条件成立.
C23 起 true 在这里换成 1, false 换成 0. 更早的 C 里它们只是 <stdbool.h> 里的宏, 没包含那个头就都是 0.
和运行时不是同一种算术
#if 里的有符号运算按 intmax_t, 无符号运算按 uintmax_t, 宽度至少 64 位. 0xFFFFFFFFu + 1 == 0 在 32 位 unsigned int 的运行时表达式里为真, 因为加法回绕. 写在 #if 里, 和是 0x100000000, 条件为假.
其它指示¶
#include¶
#include <stdio.h> 沿系统头路径找. #include "assert.h" 在 gcc 上先找当前文件所在目录, 找不到再改走尖括号那套搜索. 标准只规定引号这一路失败后要像尖括号一样再找一次, 先搜哪个目录由实现决定. 自己写的头用引号, 避免撞上同名的标准头.
C23 可以用 __has_include(<fcntl.h>) 问这个头在不在, 再决定要不要 #include.
#error, #pragma, #line¶
#error 让翻译停下来, 后面的文字是报错信息. C23 的 #warning 只出警告, 翻译继续. C23 之前 #warning 是常见扩展.
#pragma 后面写什么由编译器自己定. 不认识的 #pragma 会被忽略. gcc 的写成 #pragma GCC ..., 换一个编译器通常就当没看见. #pragma 必须自己占一行, 宏展开不出一条指示. 要在宏里面触发同样的功能, 用 C99 的 _Pragma("...").
#line 100 "generated.c" 改接下来的 __LINE__ 和 __FILE__. 生成出来的 C 代码用它, 让报错指向原来的文件.
预定义宏¶
这些不是 #define 出来的, 用在不同位置会换成不同的值.
| 名字 | 换成 |
|---|---|
__FILE__ | 当前文件名, 字符串 |
__LINE__ | 当前行号, 整数 |
__DATE__ | 编译日期, 字符串 |
__TIME__ | 编译时间, 字符串 |
__STDC_VERSION__ | 语言版本, 长整数 |
__STDC_VERSION__ 常见的值: 199901L 是 C99, 201112L 是 C11, 201710L 是 C17, 202311L 是 C23.
__func__ 不是宏, 是 C99 的预定义标识符, 在函数体内是那个函数的名字, 类型相当于 static const char 数组. 调试时和 __FILE__, __LINE__ 一起用.
assert 必须实现成宏, 表达式的类型是 void. 定义了 NDEBUG 就变成什么都不做. 下面这段把本章的展开规则串在一起:
#undef assert
#ifdef NDEBUG
#define assert(test) ((void)0)
#else
#define STR_(x) VAL_(x)
#define VAL_(x) #x
#define assert(test) ((test) ? (void)0 \
: _Assert(__FILE__ ":" STR_(__LINE__) " " #test))
#endif
main.c 第 33 行写 assert(is_sorted()) 时, __FILE__ 是 "main.c", #test 是 "is_sorted()". 直接 VAL_(__LINE__) 得到 "__LINE__", 因为 # 的实参不先展开. 多套的 STR_ 自己不用 #, 会先把 __LINE__ 展开成 33, 再交给 VAL_ 收成 "33". 相邻字符串拼起来, _Assert 收到的是 "main.c:33 is_sorted()".
_Assert 放在另一个源文件里, 自己去包含 <stdio.h> 和 <stdlib.h> 并调用 abort. assert.h 不包含它们, 这样只用断言的程序不必依赖那两个头. 标准库内部的名字以下划线开头, 那一块是留给实现的, 自己的代码不要起这样的名字.
内联函数¶
很短又调用很频繁的函数, 过去常用函数式宏去掉调用开销. C99 的 inline 是另一条路: 仍是真正的函数, 有类型检查, 实参只求值一次. 关键字只是提示编译器尽量快. 不优化时 gcc 可以照常生成 call. 加上 -O 才可能把函数体贴进调用处.
头文件里要给每个包含它的文件一份函数体时, C 里通常写 static inline. 裸的 inline 在 C 里还有一套外部定义规则, 取地址或某些调用会在链接时找不到符号. 细节见inline specifier | cppreference.