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 语句所有分支都会编译,只是运行时选择执行哪个。条件编译可排除不存在的代码,避免编译错误。

📖 小节

📝 作业

  1. 定义带括号和不带括号两个版本的 SQUARE 宏,用 SQUARE(2+3) 测试,观察结果差异
  2. #ifdef DEBUG 实现一个日志宏 LOG(fmt, ...),DEBUG 模式输出文件名和行号,非 DEBUG 模式不输出
  3. 用条件编译实现跨平台打印:Windows 输出 "Windows平台",Linux 输出 "Linux平台",用 #if defined(_WIN32) 判断
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏