源码中的 #include、#define 等行由预处理器在正式编译之前处理。预处理器按指令对文本进行替换与筛选,再将结果交给编译器。本节归纳四类常见预处理指令。
预处理器的角色
预处理器在编译器之前运行,主要完成宏展开、头文件包含、条件编译及部分杂项指令处理。其本质是文本层面的替换:例如 #define PI 3.14 会把源码中出现的标识符 PI 替换为 3.14,再进入编译阶段。
一、宏(Macros)
#define 定义宏名及其替换文本;宏定义行末尾不应加分号。
#define LIMIT 5
int main(void) {
for (int i = 0; i < LIMIT; i++) { /* LIMIT 展开为 5 */
printf("%d\n", i);
}
return 0;
}- 对象宏(object-like)
- 将标识符替换为常量或表达式,如
#define DATE 31。 - 链式宏
- 宏体可引用其他宏;展开时先处理外层再处理内层,例如
#define A B与#define B 138使A最终变为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));函数式宏的实参与整个表达式宜加括号,否则如 MIN(1+2, 3) 可能因运算符优先级产生非预期展开。宏在调用点展开,无函数调用开销,但仅做文本替换,可能引入副作用。 #undef 可取消已有宏定义。预定义宏包括 __FILE__、__LINE__、__DATE__、__TIME__ 等。
二、文件包含(File Inclusion)
#include 将指定文件内容插入当前位置;尖括号与双引号的搜索路径不同:
#include <stdio.h> /* 在系统标准目录中查找 */
#include "myfile.h" /* 通常先查当前目录,再查标准目录 */标准库头文件多用尖括号;项目自有头文件常用双引号。头文件包含是模块化与代码复用的基础,详见多文件工程与 Makefile。
三、条件编译(Conditional Compilation)
根据宏是否定义,决定某段源码是否参与后续编译,常用于平台差异、调试开关与可选功能:
#define PI 3.14159
int main(void) {
#ifdef PI
printf("PI 已定义\n");
#elif defined(SQUARE)
printf("SQUARE 已定义\n");
#else
#error "PI 和 SQUARE 都没定义"
#endif
#ifndef SQUARE
printf("SQUARE 未定义\n");
#else
printf("SQUARE 已定义\n");
#endif
return 0;
}输出:
PI 已定义
SQUARE 未定义相关指令包括 #if、#elif、#else、#endif、#ifdef、#ifndef。头文件中 #ifndef XXX_H / #define XXX_H … #endif 用于防止重复包含。
四、其他指令
#pragma:向编译器传递实现相关选项(警告、对齐、优化等),不属于可移植的 C 标准核心。#error:触发编译错误并输出自定义消息,常与条件编译配合。#line:修改编译器报告的行号与文件名,多用于代码生成工具。
本节说明了宏、包含、条件编译等预处理机制。下一节从存储类角度归纳变量在作用域、生命周期与存储位置上的差异。