C: 预处理器
预处理器就像施工前的图纸审核——在正式编译之前,先把宏展开、头文件插入、条件分支裁剪好,再交给编译器。
1. 预处理流程
C 程序的编译过程:源文件 → 预处理 → 编译 → 汇编 → 链接。预处理器在编译之前运行,处理所有 # 开头的指令。
可以用 -E 选项只看预处理结果:
BASH
gcc -E hello.c -o hello.i
2. #include 头文件
#include 将指定文件的内容原样插入到当前位置。
(1) 两种形式
C
#include <stdio.h>
#include "myheader.h"
< >在系统目录搜索" "先在当前目录搜索,找不到再搜系统目录
💡 自己写的头文件用
" ",标准库头文件用 < >,这是约定。
(2) 常用标准头文件
| 头文件 | 主要内容 |
|---|---|
stdio.h |
输入输出函数 |
stdlib.h |
内存分配、类型转换、随机数 |
string.h |
字符串操作 |
math.h |
数学函数 |
ctype.h |
字符分类 |
assert.h |
断言宏 |
3. #define 宏定义
(1) 对象宏
定义命名常量:
C
#define PI 3.14159265
#define MAX_SIZE 100
#define AUTHOR "WebTutorial"
double area = PI * 10 * 10;
int arr[MAX_SIZE];
printf("作者: %s\n", AUTHOR);
⚠️ 宏定义末尾不加分号!加了分号,分号会一起被替换,可能引发错误。
(2) 函数宏
带参数的宏,看起来像函数,但是文本替换:
C
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int s = SQUARE(5);
int m = MAX(10, 20);
TEXT
📖 仅展示
25
20
💡 预处理器只是简单文本替换,不做类型检查,不计算表达式。
4. 宏的陷阱与括号问题
(1) 陷阱一:缺少括号导致优先级错误
C
#define DOUBLE(x) x + x
C
int result = DOUBLE(3) * 2;
展开后是 3 + 3 * 2,结果是 9 而非预期的 12。正确写法:
C
#define DOUBLE(x) ((x) + (x))
(2) 陷阱二:参数含自增运算
C
#define SQUARE(x) ((x) * (x))
C
int n = 3;
int s = SQUARE(n++);
展开后是 ((n++) * (n++)),n 被自增两次,行为未定义。
⚠️ 宏参数不要传有副作用的表达式(如
i++、func()),用内联函数或真正的函数代替。
(3) 陷阱三:if-else 分支中的宏
C
#define CHECK(cond) if (cond) printf("yes\n")
C
if (flag)
CHECK(x);
else
do_something();
展开后 else 匹配的是宏内部的 if,不是外层 if——这就是经典的悬空 else 问题。用 do { ... } while(0) 包裹:
C
#define CHECK(cond) do { if (cond) printf("yes\n"); } while(0)
▶ 示例
C
#include <stdio.h>
#define SAFE_MAX(a, b) ((a) > (b) ? (a) : (b))
#define SAFE_SQUARE(x) ((x) * (x))
#define SWAP(type, a, b) do { type _tmp = (a); (a) = (b); (b) = _tmp; } while(0)
int main(void) {
int x = 10, y = 20;
printf("max: %d\n", SAFE_MAX(x, y));
printf("square: %d\n", SAFE_SQUARE(5));
SWAP(int, x, y);
printf("swap后: x=%d y=%d\n", x, y);
double a = 3.5, b = 2.1;
printf("double max: %.1f\n", SAFE_MAX(a, b));
return 0;
}
TEXT
📖 仅展示
max: 20
square: 25
swap后: x=20 y=10
double max: 3.5
5. 条件编译
条件编译让同一份源码在不同条件下编译出不同内容,广泛用于跨平台、调试开关、功能裁剪。
(1) #if / #elif / #else / #endif
C
#define PLATFORM 2
#if PLATFORM == 1
const char *os = "Windows";
#elif PLATFORM == 2
const char *os = "Linux";
#else
const char *os = "Unknown";
#endif
printf("平台: %s\n", os);
TEXT
📖 仅展示
平台: Linux
(2) #ifdef / #ifndef
#ifdef 检查宏是否已定义,#ifndef 检查是否未定义。
C
#define DEBUG
#ifdef DEBUG
printf("调试模式: x=%d\n", x);
#endif
#ifndef BUFFER_SIZE
#define BUFFER_SIZE 256
#endif
💡
#ifdef DEBUG 等价于 #if defined(DEBUG),#ifndef 等价于 #if !defined(...)。
(3) 调试开关
C
#ifdef DEBUG
#define LOG(msg) printf("[DEBUG] %s:%d %s\n", __FILE__, __LINE__, msg)
#else
#define LOG(msg)
#endif
LOG("变量初始化完成");
编译时加 -DDEBUG 就开启日志,不加就静默:
BASH
gcc -DDEBUG -o myapp myapp.c
▶ 示例
C
#include <stdio.h>
#define LEVEL 3
int main(void) {
#if LEVEL >= 3
printf("高级功能已启用\n");
#elif LEVEL >= 2
printf("中级功能已启用\n");
#else
printf("基础功能\n");
#endif
#ifdef VERBOSE
printf("详细输出模式\n");
#else
printf("简洁输出模式\n");
#endif
return 0;
}
TEXT
📖 仅展示
高级功能已启用
简洁输出模式
6. 预定义宏
C 语言标准预定义了几个有用的宏:
| 宏 | 含义 |
|---|---|
__FILE__ |
当前源文件名 |
__LINE__ |
当前行号 |
__DATE__ |
编译日期 |
__TIME__ |
编译时间 |
__func__ |
当前函数名(C99) |
__STDC__ |
是否符合 ANSI C 标准 |
C
printf("文件: %s\n", __FILE__);
printf("行号: %d\n", __LINE__);
printf("日期: %s\n", __DATE__);
printf("时间: %s\n", __TIME__);
printf("函数: %s\n", __func__);
TEXT
📖 仅展示
文件: main.c
行号: 5
日期: Jun 25 2026
时间: 14:30:00
函数: main
💡 自定义日志宏常结合预定义宏使用,输出文件名、行号等定位信息。
7. #undef 取消宏
#undef 取消之前的宏定义:
C
#define MAX_SIZE 100
printf("%d\n", MAX_SIZE);
#undef MAX_SIZE
取消后 MAX_SIZE 不再是宏,可以重新定义或作为普通标识符使用。
❓ 常见问题
Q 宏定义为什么不加分号?
A 宏是文本替换,加分号会导致分号成为替换内容的一部分,在 if-else 等上下文中容易引发语法错误。
Q 函数宏和内联函数怎么选?
A 内联函数有类型检查、参数只求值一次、调试方便。函数宏灵活但不安全。能用内联函数就用内联函数,宏用于类型泛型(如 SWAP)等特殊场景。
Q #ifdef 和 #if defined 有区别吗?
A 功能相同,
#if defined 更灵活,可以组合多个条件:#if defined(A) && defined(B),#ifdef 只能检查单个宏。Q 条件编译和 if 语句有什么区别?
A 条件编译在预处理阶段裁剪代码,未选中的分支根本不会进入编译;if 语句所有分支都会编译,只是运行时选择执行哪个。条件编译可排除不存在的代码,避免编译错误。
📖 小节
#include将头文件内容插入源文件,< >搜系统目录," "搜当前目录#define定义对象宏(常量替换)和函数宏(带参数的文本替换)- 宏参数必须加括号,整体也要加括号,避免优先级陷阱
- 宏参数不要传有副作用的表达式
- 条件编译
#if/#ifdef/#ifndef实现跨平台、调试开关、功能裁剪 - 预定义宏
__FILE__、__LINE__、__DATE__等用于日志和调试
📝 作业
- 定义带括号和不带括号两个版本的 SQUARE 宏,用
SQUARE(2+3)测试,观察结果差异 - 用
#ifdef DEBUG实现一个日志宏LOG(fmt, ...),DEBUG 模式输出文件名和行号,非 DEBUG 模式不输出 - 用条件编译实现跨平台打印:Windows 输出 "Windows平台",Linux 输出 "Linux平台",用
#if defined(_WIN32)判断