源码中的 #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:修改编译器报告的行号与文件名,多用于代码生成工具。

本节说明了宏、包含、条件编译等预处理机制。下一节从存储类角度归纳变量在作用域、生命周期与存储位置上的差异。