这一章在干嘛?
预处理阶段的全套指令:#define 的替换规则与宏的副作用、宏 vs 函数的取舍、条件编译、文件包含与头文件守卫。宏是 C 最锋利也最容易割手的部分。
14.1 define:替换规则与副作用
#define MAX_COLS 20 /* 符号常量 */
#define SQUARE(x) ((x) * (x)) /* 宏:名字(参数) 替换体 —— 中间不能有空格 */
int a = MAX_COLS; /* 替换为 20 */
int b = SQUARE(3); /* 替换为 ((3) * (3)) = 9 */
替换是纯文本操作:宏参数在替换体里每次出现都被原文替换。不加括号的后果:
#define BAD(x) x * x
int c = BAD(1 + 2); /* 变成 1 + 2 * 1 + 2 = 5,而不是 9! */
int d = SQUARE(1 + 2); /* ((1+2)*(1+2)) = 9 —— 参数加括号 */
int n = 5;
int e = SQUARE(n++); /* ((n++)*(n++)):同一序列点两次修改,未定义! */
宏的两大军规:
① 替换体里每个参数、整体结果都套括号;② 参数在替换体中只出现一次最安全——出现多次且调用方传 ++/—,就是副作用未定义行为。
14.2 宏与函数:怎么选
| 维度 | define 宏 | 函数 |
|---|---|---|
| 开销 | 零调用开销,代码膨胀(每处展开) | 调用开销,代码一份 |
| 类型检查 | 无——文本替换,任何类型都能套 | 严格原型检查 |
| 副作用 | 参数重复求值(++ 危险) | 参数只求值一次 |
| 类型灵活性 | 天然泛型(对任何类型都「对」) | 类型固定(或用 void* 模拟) |
| 适用 | 短小、频繁、想要任意类型的计算 | 逻辑复杂、参数有副作用、需要递归 |
两个实用补充:#undef NAME 取消定义(重定义前必须先 undef);#define 名称 只判断「是否定义过」用于条件编译。命名约定:宏全大写,让调用方一眼识别「这是替换、小心副作用」。
14.3 条件编译与文件包含
#if defined(DEBUG) && DEBUG >= 2 /* 表达式形式 */
...调试代码...
#elif defined(TRACE)
...精简日志...
#endif
#ifndef DEBUG /* 是否被定义的简写 */
#define DEBUG 1
#endif
/* 头文件守卫:防止重复包含导致重定义 */
#ifndef POINTS_ON_C_LIST_H
#define POINTS_ON_C_LIST_H
/* ...头文件内容:原型、类型定义、宏... */
#endif
头文件里只放声明(原型、类型、宏、extern 变量),不放定义(函数体、变量实体)——定义放 .c 文件,否则被两个单元包含就重定义。#include 嵌套靠守卫兜底。
本章通关标准:
看到 SQUARE(n++) 能指出未定义行为;写头文件条件反射加守卫;分清 <> 和 "" 的查找路径。
1. define SQUARE(x) x*x 错在哪?正确写法?
纯文本替换导致结合错误:SQUARE(1+2) 展开为 1+21+2=5。正确写法给参数和整体都加括号:#define SQUARE(x) ((x)(x))。即便如此,传 ++/— 参数仍会因重复求值产生未定义行为。
2. 什么场景选宏、什么场景选函数?
宏适合:极短的计算、需要任意类型复用(天然泛型)、要编译期常量。函数适合:逻辑较长、参数有副作用(函数参数只求值一次)、需要类型检查或递归。有副作用风险的调用一律避开宏。
3. 头文件守卫怎么写、防什么?