每次写 #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,学会了它,你再看大型项目的那些宏定义和平台分支就有底了。