C 语言预处理、宏与调试
更新时间:2026-08-25。本文是
languages/c/主题高手层第 4 篇。预处理是编译的第一阶段,宏是 C 的"文本替换"利器也是最容易埋雷的地方;调试是每个 C 程序员的日常。本文把这两件事讲透。
本文要回答的问题
#define宏和函数有什么区别?宏有哪些隐蔽的坑?#ifdef条件编译有什么用?什么时候用?- 除了
printf打日志,gdb 怎么用?
一、预处理指令总览
预处理是编译四阶段的第一步(见 编译、链接与 ELF),处理以 # 开头的指令:
| 指令 | 作用 |
|---|---|
#include | 插入头文件内容 |
#define | 定义宏(常量或函数式) |
#undef | 取消宏定义 |
#ifdef / #ifndef / #endif | 条件编译 |
#if / #elif / #else | 条件表达式 |
#pragma | 编译器特定指令 |
用 gcc -E file.c 可以看到预处理后的结果(展开所有宏和 include)。
二、宏:#define
2.1 对象宏(常量)
c
#define MAX_SIZE 1024
#define PI 3.14159
int buf[MAX_SIZE];预处理就是文本替换——MAX_SIZE 在编译前被替换成 1024。
2.2 函数式宏(有参数的宏)
c
#define SQUARE(x) ((x) * (x))
int r = SQUARE(5); // 展开成 ((5) * (5))2.3 宏的三大坑(重点)
坑 1:不加大括号导致运算错误
c
#define SQUARE(x) x * x // 危险!
SQUARE(1 + 2); // 展开成 1 + 2 * 1 + 2 = 5,而不是 9!正确写法:参数和整体都加括号 #define SQUARE(x) ((x) * (x))。
坑 2:副作用被多次求值
c
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = 5;
MAX(x++, 10); // x++ 可能被求值两次,结果不可预测!宏参数若有副作用(x++),会被多次求值——这是宏和函数最大的区别。
坑 3:类型不安全
宏不做类型检查,传什么类型都行,出错难排查。
2.4 宏 vs 函数 vs 内联
| 特性 | 宏 | 函数 | inline 函数 |
|---|---|---|---|
| 类型检查 | 无 | 有 | 有 |
| 副作用安全 | 否 | 是 | 是 |
| 调用开销 | 无(文本展开) | 有(调用/返回) | 无(内联展开) |
| 调试 | 难 | 易 | 较易 |
建议:现代 C 尽量用 inline 函数替代函数式宏,只在确实需要"编译期文本替换"时用宏。
三、条件编译
3.1 #ifdef / #ifndef
c
#ifdef DEBUG
printf("调试信息\n");
#endif编译时加 -DDEBUG 就启用这段,不加就跳过。常用于:
- 调试代码(发布时关闭)
- 跨平台代码(
#ifdef _WIN32/#ifdef __linux__)
3.2 #if 条件表达式
c
#if defined(__linux__)
// Linux 专用代码
#elif defined(_WIN32)
// Windows 专用代码
#else
// 其他平台
#endif四、断言 assert
c
#include <assert.h>
int divide(int a, int b) {
assert(b != 0); // b 为 0 时程序在这里崩溃,并打印位置
return a / b;
}assert在NDEBUG定义时会被禁用(gcc -DNDEBUG)。- 用于"检查不应发生的逻辑错误",不是用来处理运行时可预期的错误(如用户输入)。
五、调试器 gdb 入门
5.1 编译时加 -g
bash
gcc -g -O0 program.c -o program # -g 保留调试信息5.2 gdb 核心命令
bash
gdb ./program # 启动 gdb
(gdb) break main # 在 main 处设断点
(gdb) run # 运行
(gdb) next # 单步(不进入函数)
(gdb) step # 单步(进入函数)
(gdb) print x # 打印变量 x
(gdb) backtrace # 查看调用栈
(gdb) continue # 继续执行
(gdb) quit # 退出5.3 段错误定位
bash
gdb ./program
(gdb) run # 程序崩溃后
(gdb) backtrace # 看崩溃时的调用栈,定位到具体行gdb 完整用法见 crash 排查线,core dump 分析见 core dump 排查。
六、调试打印技巧
| 技巧 | 说明 |
|---|---|
fprintf(stderr, ...) | 走错误流,不被 stdout 重定向干扰 |
__FILE__ / __LINE__ / __func__ | 预定义宏,打印位置 |
| 宏包装调试打印 | #define LOG(fmt, ...) fprintf(stderr, ...) |
c
#define DEBUG_LOG(fmt, ...) \
fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)七、与本站主线衔接
- 预处理与编译:宏展开是编译第一阶段,见 编译、链接与 ELF。
- -g 与符号:
-g产生的调试信息让 perf/gdb 定位到行号,见 L5 ELF。 - 崩溃排查:gdb + core dump 完整流程,见 crash 排查线。
一句话总结
预处理与调试 = 记住宏是"文本替换"(三大坑:括号/副作用/无类型)、条件编译用 #ifdef、调试靠 assert + gdb + 调试打印;宏谨慎用、gdb 熟练用,C 的 bug 就藏不住。
上一篇:文件 IO 与模块化 下一篇:编译、链接与 ELF