📖 第 14 章:预处理器(The Preprocessor)

🔤 Kenneth Reek《Pointers on C》(C和指针)· Chapter 14
🗨️ 本章金句:「预处理只是文本替换——它不懂 C 的语法,所以它干的每一件事都要按替换的思路想一遍。」
这一章在干嘛? 预处理阶段的全套指令:#define 的替换规则与宏的副作用、宏 vs 函数的取舍、条件编译、文件包含与头文件守卫。宏是 C 最锋利也最容易割手的部分。
14.1 #define:替换规则与副作用14.2 宏与函数:怎么选14.3 条件编译与文件包含

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
写法含义用途
#include <stdio.h>到系统目录找标准库/第三方
#include "list.h"先当前目录再系统目录自己的头文件

头文件里只放声明(原型、类型、宏、extern 变量),不放定义(函数体、变量实体)——定义放 .c 文件,否则被两个单元包含就重定义。#include 嵌套靠守卫兜底。

本章通关标准: 看到 SQUARE(n++) 能指出未定义行为;写头文件条件反射加守卫;分清 <> 和 "" 的查找路径。
🧠 小测验
1. #define SQUARE(x) x*x 错在哪?正确写法?
纯文本替换导致结合错误:SQUARE(1+2) 展开为 1+2*1+2=5。正确写法给参数和整体都加括号:#define SQUARE(x) ((x)*(x))。即便如此,传 ++/-- 参数仍会因重复求值产生未定义行为。
2. 什么场景选宏、什么场景选函数?
宏适合:极短的计算、需要任意类型复用(天然泛型)、要编译期常量。函数适合:逻辑较长、参数有副作用(函数参数只求值一次)、需要类型检查或递归。有副作用风险的调用一律避开宏。
3. 头文件守卫怎么写、防什么?
#ifndef UNIQUE_NAME / #define / #endif 包住整个头文件,防止同一翻译单元重复包含(如两个头互相包含或同一头被包含多次)导致的类型/宏重定义编译错误。守卫宏名按文件路径取名保证唯一。
← 上一篇🏠 顶层目录下一篇 →