C 语言宏定义 #define
更新时间:2026-08-26。本文是
languages/c/主题入门层第 56 篇。#define是预处理阶段最常用的武器:定义常量、写"伪函数"。但它本质是文本替换——不是函数、没有类型、没有作用域,用错了就是经典的#define SQUARE(x) x*x翻车现场。这篇把规则和陷阱一次讲清。
本文要回答的问题
- 宏在什么时候展开?为什么它是"文本替换"?
- 带参宏有哪些经典陷阱?
- 什么场景该用宏,什么场景该用函数/enum?
一、宏是预处理的文本替换
c
#define PI 3.14159
#define MAX_ITEMS 100gcc -E 看预处理结果,PI、MAX_ITEMS 会被原样替换成文本。在编译器看到代码之前,宏就已经消失了——所以:
- 宏没有类型、没有作用域、不占内存;
- 调试器里看不到宏名(
gdb需要-g3才显示宏); - 宏替换可能产生意想不到的展开。
二、对象宏与带参宏
对象宏(无参数):
c
#define BUFFER_SIZE 4096
#define DEFAULT_NAME "anonymous"带参宏(函数式宏):
c
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))调用 SQUARE(5) 会在预处理阶段变成 ((5) * (5))。
三、经典陷阱 1:括号问题
c
#define SQUARE(x) x * x // ❌
int y = SQUARE(2 + 3); // 变成 2 + 3 * 2 + 3 = 11,不是 25!原因:SQUARE(2+3) → 2 + 3 * 2 + 3,运算优先级全乱。
规则:
- 每个参数加括号:
((x) * (x)); - 整个展开结果加括号:
((a) + (b))。
c
#define SQUARE(x) ((x) * (x)) // ✅
#define SUM(a, b) ((a) + (b)) // ✅四、经典陷阱 2:副作用
c
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = 3;
int m = MAX(x++, 5);
// 展开:((x++) > (5) ? (x++) : (5))
// x++ 执行了两次!参数被多次求值——x++、f() 这类带副作用表达式在宏里会重复执行。这是宏与函数的本质区别(函数参数只求值一次)。
五、经典陷阱 3:多语句宏
c
#define SWAP(a, b) { int t = a; a = b; b = t; } // ❌ 有隐患
if (cond)
SWAP(x, y); // 展开成 { ... }; → if 后面跟了块加个分号
else
...用 do-while(0) 技巧包装,让宏像一条语句:
c
#define SWAP(a, b) do { \
int t = (a); (a) = (b); (b) = t; \
} while (0)do {...} while(0) 是标准惯用法:块内多语句安全、结尾分号无副作用、不会吞掉 else。
六、宏 vs 函数 vs enum/const
| 场景 | 推荐 |
|---|---|
| 常量 | enum / const(有类型检查),除非是编译期数组大小 |
| 小运算(取大/平方) | 宏(无调用开销)或 static inline 函数 |
| 复杂逻辑 | 函数 |
| 编译期开关 | 宏(配合条件编译) |
调试信息(__FILE__/__LINE__) | 宏(函数拿不到) |
结论:能用函数/枚举就别用宏;宏留给"真需要文本替换"的场合——条件编译、代码生成、调试日志。
七、常见坑对照
| 坑 | 现象 | 对策 |
|---|---|---|
| 忘了括号 | 优先级错误 | 参数和整体都加括号 |
| 参数多次求值 | 副作用重复 | 不用带副作用参数/改用函数 |
| 多语句宏 | if-else 错乱 | do-while(0) 包装 |
| 行末反斜杠后空格 | 宏延续失败 | 反斜杠必须行尾最后字符 |
| 与函数同名 | 意外替换 | 命名规范(全大写) |
八、与本站主线衔接
- 宏与条件编译组合控制编译行为,见条件编译;
- 预定义宏与断言,见预定义宏与 assert;
- 宏的高级用法(X-macro 等),见预处理进阶。
一句话总结
#define 是编译前的文本替换:常量用 enum/const、运算用函数,真需要宏时牢记三规则——参数加括号、整体加括号、多语句用 do-while(0);宏是把双刃剑,用对了是编译期魔法,用错了是夜里三点多的段错误。