本节摘要:预处理器在编译前跑一遍"文本搬运"(第 1 章第一站),本章把它当作工程组织工具展开:对象宏与函数宏的规矩、宏副作用防治、头文件的包含保护与依赖管理、条件编译组织多平台代码与配置开关。宏是把双刃剑——用对了是零成本抽象,用错了是调试地狱。
第 1.2 节讲过预处理器三件事:宏替换、文件包含、条件选择。工程视角逐个展开。
对象宏定义常量与配置:
#define MAX_USERS 1024 #define VERSION "2.1.0" #define PI 3.14159265358979
函数宏必须加括号——第 1.2 节见过 SQUARE(a+1) 的事故。完整规矩版:
/* 全体参数加括号,整体表达式加括号 */ #define SQUARE(x) ((x) * (x)) #define MAX(a, b) ((a) > (b) ? (a) : (b))
括号只解决优先级,解决不了参数重复求值:MAX(i++, j) 展开后 i++ 出现两次,自增两回——副作用在宏里被放大。防治三选一:
现代 C 的正解是能不用函数宏就不用:常量用 enum 或 const(有类型、可调试),短函数加 inline。函数宏留给"必须类型泛型"的场合(容器、比较器)。
#undef 撤销定义,常用于"临时重定义前先拆旧";预定义宏 __FILE__、__LINE__、__DATE__、__STDC_VERSION__ 用于日志与版本信息——它们在编译时替换成当前文件名与行号,是排查利器。
头文件的职责清单:函数原型、类型定义、宏、外部声明。四不放:函数实现(重复定义)、变量定义(同理)、不必要的包含(传染性依赖)、using 类的污染性声明。
包含保护是每个头文件的第一二行:
#ifndef MATHUTIL_H #define MATHUTIL_H int add(int a, int b); long factorial(int n); #endif /* MATHUTIL_H */
ifndef 检查加 define 标记,同一编译单元里重复包含时第二次起整个文件被条件筛掉。主流编译器也支持 #pragma once,效果等价且不用起宏名——团队统一选一种即可。
包含顺序有讲究:对应的自身头文件放最前(让本文件的头独立可编译,缺失依赖当场暴露)、C 标准库、第三方库、本项目头文件。双向包含是设计坏味道:A 头要 B 的类型、B 头要 A 的类型,解法是前置声明(struct B; 只声明不定义)或拆出第三个公共头。
#include <stdio.h> int main(void) { #if defined(_WIN32) printf("Windows 平台\n"); #elif defined(__APPLE__) printf("macOS 平台\n"); #elif defined(__linux__) printf("Linux 平台\n"); #else printf("未知平台\n"); #endif #ifdef DEBUG printf("调试构建,第 %d 行\n", __LINE__); #endif return 0; }
条件编译让平台差异、调试开关、功能裁剪共存于一份源码。编译器命令行传宏定义(形如 -DDEBUG),不改代码就能切换构建形态。惯用套路还有:
/* 特性开关:默认开,构建时可关 */ #ifndef ENABLE_LOG #define ENABLE_LOG 1 #endif #if ENABLE_LOG log_message("..."); #endif /* 排除式保护:某些头缺函数原型时自己补 */ #if !defined(HAVE_STRDUP) char *strdup(const char *s); #endif

| 需求 | 首选 | 理由 |
|---|---|---|
| 数值常量 | enum 或 const | 有类型 可调试 作用域安全 |
| 字符串常量 | 宏 或 const 数组 | 宏更传统,const 更安全 |
| 类型泛型短操作 | 函数宏 | C 没有泛型函数 |
| 带副作用的参数 | 函数 | 宏会重复求值 |
| 性能关键短函数 | inline 函数 | 现代编译器一样内联 |
| 平台与配置开关 | 条件编译 | 唯一选择 |
⚠️ 常见坑:用分号结尾的宏
#define MAX 100;——展开后多出的分号破坏表达式与 if 结构,报错位置离案发现场十万八千里。宏定义不写分号,是铁律。
💡 关键直觉:宏是"文本复印机",不是代码。它没有类型、没有作用域、没有求值语义——每一行宏都问自己"展开后是什么文本",答案对了宏才对。
下一节是全书的最后一站:错误处理、调试与工程规范——把机器知识变成可靠的交付能力。
问:宏为什么不能递归展开? 展开过程中同名宏会被标记为不再展开,防止无限递归。这也是间接宏的经典用途:让参数先经过一层中间宏再展开,解决某些拼接场景。
问:条件编译能不能用普通 if 代替? 不能等价替换。if 语句两个分支都要通过编译(未选中的平台代码可能编不过),条件编译则把未选分支在文本层面删除。涉及平台专有头文件与接口时,只有条件编译可行。
问:头文件包含太多会怎样? 编译变慢、依赖传染。改动一个被广泛包含的头,会触发大量文件重编。解法是前置声明减少包含、按模块聚合、必要时用预编译头加速。