预处理与宏:#define、#include、条件编译、#与##运算符、宏的陷阱
预处理,说白了就是编译器在正式编译之前,先帮你做的一轮文本替换工作。很多新手觉得这玩意儿就是简单的「查找替换」,其实不然。我做了这么多年嵌入式,见过太多因为宏定义写得不严谨,导致线上产品出bug的案例。今天咱们就把预处理这块掰开揉碎了讲清楚。
一、#define:不只是简单的文本替换
先说说最常用的 #define。它的本质就是文本替换,但很多人容易忽略括号的重要性。
// 错误示范
#define SQUARE(x) x * x
// 正确写法
#define SQUARE(x) ((x) * (x))
int a = 5;
int b = SQUARE(a + 1); // 错误:5+1*5+1 = 11,不是36
int c = SQUARE(a + 1); // 正确:((5+1)*(5+1)) = 36
我在项目中遇到过有人写了一个宏来计算两个数的最大值:
#define MAX(a, b) ((a) > (b) ? (a) : (b))
看起来没问题对吧?但如果你这么用:
int x = 5, y = 3;
int z = MAX(++x, y); // 结果:x被自增了两次!
嗯,这里要注意:宏的参数会被多次求值。所以带副作用的参数,千万别往宏里传。
二、#include:头文件包含的艺术
#include 说白了就是把另一个文件的内容原封不动地复制过来。但这里有个大坑——重复包含。
你想想看,如果头文件A包含了头文件B,头文件C也包含了头文件B,然后你的源文件同时包含了A和C,那B就被包含了两次。结构体重复定义,编译器直接报错。
解决方案很简单,用头文件保护宏:
// my_header.h
#ifndef MY_HEADER_H
#define MY_HEADER_H
// 你的代码
#endif // MY_HEADER_H
我个人习惯在每一个头文件里都加上这个保护,不管它会不会被重复包含。这是一种防御性编程思维。
三、条件编译:让一份代码适配多种平台
条件编译是嵌入式开发的利器。我做过一个项目,同一套代码要跑在STM32和GD32两种芯片上,靠的就是 #ifdef 和 #ifndef。
#ifdef STM32_PLATFORM
#include "stm32_hal.h"
#define LED_PIN GPIO_PIN_13
#elif defined(GD32_PLATFORM)
#include "gd32_hal.h"
#define LED_PIN GPIO_PIN_5
#else
#error "Unknown platform!"
#endif
这里有个小技巧:用 #if 而不是 #ifdef 来做条件判断,因为 #if 支持更复杂的表达式:
#define VERSION_MAJOR 2
#define VERSION_MINOR 1
#if VERSION_MAJOR > 1
// 版本大于1时的代码
#endif
#if (VERSION_MAJOR == 2) && (VERSION_MINOR >= 1)
// 版本2.1及以上的代码
#endif
四、# 和 ## 运算符:字符串化和连接
这两个运算符用得不多,但用好了能写出很优雅的代码。
# 运算符把宏参数变成字符串:
#define STR(x) #x
printf(STR(hello world)); // 等价于 printf("hello world");
我在调试日志里经常这么用:
#define LOG_VAR(var) printf(#var " = %d\n", var)
int temperature = 25;
LOG_VAR(temperature); // 输出:temperature = 25
## 运算符把两个符号连接成一个:
#define MAKE_FUNC(name) void func_##name(void)
MAKE_FUNC(init); // 展开为:void func_init(void);
MAKE_FUNC(reset); // 展开为:void func_reset(void);
你想想看,如果有一组结构体,每个结构体都有对应的初始化函数,用 ## 可以批量生成函数声明,省去大量重复劳动。
五、宏的陷阱:那些年我们踩过的坑
做嵌入式这么多年,我总结了几条宏的常见陷阱:
| 陷阱类型 | 示例 | 后果 |
|---|---|---|
| 缺少括号 | #define ADD(x,y) x+y |
运算优先级错误 |
| 参数多次求值 | #define MAX(a,b) ((a)>(b)?(a):(b)) |
传入++x时行为异常 |
| 分号问题 | #define DO_SOMETHING() func1(); func2(); |
if-else结构被破坏 |
| 宏名冲突 | 两个头文件定义了同名宏 | 编译错误或逻辑错误 |
关于分号问题,我举个例子:
#define BAD_MACRO() func1(); func2()
if (condition)
BAD_MACRO();
else
// 这里会编译报错!
展开后变成:
if (condition)
func1();
func2();
else // else 没有对应的 if,报错!
正确的做法是用 do-while(0) 包裹:
#define GOOD_MACRO() do { func1(); func2(); } while(0)
六、知识体系总览
下面这张图把本章的核心知识点串了起来,方便你建立整体认知:
预处理看似简单,但用好了能写出高效、可移植的代码,用不好就是一颗定时炸弹。我个人建议:能用 const 和 enum 的地方,就别用 #define 定义常量;能用内联函数的地方,就别用宏实现逻辑。预处理器的正确角色,应该是「编译前的文本处理工具」,而不是「函数替代品」。
记住一句话:宏是双刃剑,用之前先想想有没有更好的替代方案。