每次写 #include <stdio.h> 其实都在和预处理器打交道。它是编译的第一步:在真正的编译器动手之前,先按指令把源码改写一遍。下面看它的四类指令。
预处理器是什么
预处理器在编译之前运行,干四件事:展开宏、包含头文件、条件编译,以及处理 #pragma 这类杂项。它只是文本替换,比如 #define PI 3.14 会把源码里每个 PI 原样换成 3.14,然后才交给编译器。
一、宏(Macros)
#define 定义一个宏,编译器开始前把宏名替换成它的内容。宏定义行尾没有分号,这是规矩。
#define LIMIT 5
int main() {
for (int i = 0; i < LIMIT; i++) { /* LIMIT 会被换成 5 */
printf("%d\n", i);
}
return 0;
}- 对象宏(object-like)
- 最简单的名字换值:
#define DATE 31把所有 DATE 变成 31。 - 链式宏(chain)
- 一个宏里套另一个宏,先展开父再展开子:
#define A B+#define B 138,用 A 时先变 B 再变 138。 - 多行宏
- 用行尾的反斜杠
\续行:
#define ELE 1, \
2, \
3
int arr[] = {ELE}; /* 展开成 {1, 2, 3} */函数式宏(function-like),带参数的宏,展开时参数原样放进去:
#define MIN(a, b) (((a) < (b)) ? (a) : (b))
int a = 18, b = 76;
printf("最小值:%d\n", MIN(a, b)); // 18注意函数式宏的坑:参数和整个表达式都要包括号,否则 MIN(1+2, 3) 会被展开成奇怪的东西。它比真函数省一次调用开销,但也因为只是文本替换,容易产生意想不到的副作用。
#undef 可以撤销宏:撤销后再用那个名字,直接编译报错。预处理器还内置了几个现成的宏,比如 __FILE__(当前文件名)、__LINE__(当前行号)、__DATE__、__TIME__。
二、文件包含(File Inclusion)
#include 把别的文件内容整个粘进来,两种写法查找范围不同:
#include <stdio.h> /* 尖括号:只去系统标准目录找 */
#include "myfile.h" /* 双引号:先找当前目录,找不到再去标准目录 */标准库的 printf、scanf 靠它;自己写的头文件用双引号。它是代码复用和模块化的基础。
三、条件编译(Conditional Compilation)
让某段代码编不编在预处理期决定,常用于平台差异、调试开关、可选功能:
#define PI 3.14159
int main() {
#ifdef PI /* 如果定义了 PI */
printf("PI 已定义\n");
#elif defined(SQUARE)
printf("SQUARE 已定义\n");
#else
#error "PI 和 SQUARE 都没定义"
#endif
#ifndef SQUARE /* 如果没定义 SQUARE */
printf("SQUARE 未定义\n");
#else
printf("SQUARE 已定义\n");
#endif
return 0;
}输出:
PI 已定义
SQUARE 未定义配套指令:#if、#elif、#else、#endif、#ifdef(定义了才编)、#ifndef(没定义才编)。#endif 用来收尾。头文件里那句著名的 #ifndef XXX_H / #define XXX_H ... #endif 防重复包含,就是这个家族。
四、其他指令
#pragma:给编译器的私有指令(警告、对齐、优化),各家编译器支持不一,不属于 C 标准。#error:带自定义信息直接报编译错误,常配着条件编译一起阻止编译。#line:改编译器报告的行号和文件名,代码生成工具用得多。
预处理器把编译前改写源码这套思路给了 C,学会了它,你再看大型项目的那些宏定义和平台分支就有底了。