宏定义是 C 语言预处理阶段的核心功能,通过#define实现纯文本替换,不占用运行时资源,在代码复用、常量定义、条件编译等场景中不可或缺。下面按「基础语法」→「高级特性」→「实用场景」→「注意事项」的逻辑,全面梳理宏的用法。
一、 基础宏定义:无参数宏(对象宏)
1. 语法格式
#define 宏名 替换文本 // 末尾无分号(避免多余文本插入)
2. 核心功能
将代码中所有出现「宏名」的标识符,在预处理阶段直接替换为「替换文本」,常用于定义常量、魔数别名、代码片段缩写。
3. 示例与注意事项
(1) 定义常量(推荐宏名大写,区分普通变量)
// 1. 数值常量(硬件参数、阈值等)
#define PI 3.1415926
#define BAUD_RATE 115200 // 串口波特率
#define KEY_INTERVAL 100 // 你代码中的按键间隔
// 2. 字符串常量
#define LOG_TAG "ADC_DEBUG"
#define DEFAULT_CONFIG "/etc/config.ini"
// 3. 代码片段缩写(简化重复操作)
#define DELAY_MS(x) usleep((x)*1000) // 毫秒延时(基于usleep)
(2) 关键注意事项:加括号避免运算优先级问题
无参数宏的替换是「原样替换」,若替换文本包含运算,必须用括号包裹,否则会因运算符优先级导致意外错误。
// 错误示例:无括号,运算优先级异常
#define M 2 + 3
int a = M * 5; // 替换后:a = 2 + 3 * 5 → 结果17(预期25)
// 正确示例:替换文本整体加括号
#define M (2 + 3)
int a = M * 5; // 替换后:a = (2 + 3) * 5 → 结果25(符合预期)
(3) 空宏定义
用于条件编译标记,无实际替换文本:
#define DEBUG // 标记调试模式开启
#define TAG_DISABLE // 代码中的空宏,用于禁用日志
二、 核心宏定义:带参数宏(函数宏)
1. 语法格式
#define 宏名(参数列表) 替换文本 // 参数列表无类型声明(C语言无类型检查)
2. 核心功能
模拟函数调用,实现「代码片段的参数化复用」,预处理阶段完成替换,无函数调用的栈开销。
3. 示例与关键技巧
(1) 基础示例:简单运算
// 加法宏
#define ADD(a, b) ((a) + (b))
// 最大值宏
#define MAX(a, b) ((a) > (b) ? (a) : (b))
// 最小值宏
#define MIN(a, b) ((a) < (b) ? (a) : (b))
(2) 必做优化:参数 + 整体双重加括号
带参数宏的替换易受运算优先级影响,必须满足两个要求:
-
每个参数单独用括号包裹(避免参数本身是表达式时出错)
-
整个替换文本用括号包裹(避免宏作为表达式一部分时出错)
// 错误示例:无双重括号
#define MULTIPLY(a, b) a * b
int x = MULTIPLY(2 + 3, 4 + 5); // 替换后:2 + 3 * 4 + 5 → 结果19(预期45)// 正确示例:双重括号
#define MULTIPLY(a, b) ((a) * (b))
int x = MULTIPLY(2 + 3, 4 + 5); // 替换后:((2 + 3) * (4 + 5)) → 结果45
(3) 避免宏副作用
宏参数若为「自增 / 自减表达式」(如i++、--j),会因多次替换导致意外结果(函数调用仅执行一次参数表达式):
// 副作用示例
#define INC(a) ((a)++)
int i = 1;
int b = INC(i + 1); // 替换后:((i + 1)++) → 语法错误(表达式不能自增)
int c = INC(i); // 若宏内部多次使用a,i会被多次自增
// 规避方案:尽量使用简单参数(无自增/自减、无函数调用)
三、 宏的高级预处理操作符
C 语言提供 3 个特殊操作符,用于增强宏的功能,也是嵌入式开发中批量配置、日志打印的核心技巧。
1. 字符串化操作符:#
(1) 语法与功能
在带参数宏中,# 可将「宏参数」直接转换为带双引号的字符串常量,无需手动添加引号。
(2) 示例(对应你代码中的DEFINE_ADC_VOLTAGE)
// 将_TAG转为字符串
#define DEFINE_ADC_VOLTAGE(_TAG, ADC, FUNC) {#_TAG, ADC, FUNC},
// 基础示例
#define STR(x) #x
printf("%s\n", STR(123)); // 替换后:printf("%s\n", "123");
printf("%s\n", STR(abc)); // 替换后:printf("%s\n", "abc");
printf("%s\n", STR(ADC_123)); // 替换后:printf("%s\n", "ADC_123");
2. 连接操作符:##
(1) 语法与功能
将「两个标识符」拼接为一个新的标识符(无分隔符),支持宏参数与普通标识符拼接,常用于批量定义变量 / 函数。
(2) 示例
// 基础拼接
#define CONCAT(a, b) a##b
int CONCAT(abc, 123); // 替换后:int abc123;
CONCAT(printf, _str)("hello"); // 替换后:printf_str("hello");
// 批量定义GPIO函数(嵌入式常用)
#define GPIO_FUNC(pin) GpioLevelSet_##pin
void GpioLevelSet_60(int level); // 引脚60的设置函数
GPIO_FUNC(60)(1); // 替换后:GpioLevelSet_60(1);
3. 可变参数操作符:... 与 __VA_ARGS__
(1) 语法与功能
...:在宏参数列表末尾,用于接收「不定数量的可变参数」(与printf格式一致)__VA_ARGS__:在替换文本中,用于替代...接收的所有可变参数- 扩展用法:
##__VA_ARGS__:解决可变参数为空时的编译错误(自动省略多余的逗号)
(2) 示例(对应你代码中的TAG_DISABLE)
// 空宏,禁用日志
#define TAG_DISABLE(format, ...)
// 实用示例1:调试日志宏(开启/关闭可控)
#ifdef DEBUG
#define LOG_INFO(format, ...) printf("[INFO] " format "\n", ##__VA_ARGS__)
#else
#define LOG_INFO(format, ...) // 发布模式禁用日志
#endif
// 使用
LOG_INFO("ADC电压:%d mV,标签:%s", 1715, "Call1");
// 调试模式替换后:printf("[INFO] ADC电压:%d mV,标签:%s\n", 1715, "Call1");
// 发布模式替换后:无任何代码
// 实用示例2:无格式字符串的可变参数
#define LOG_ERROR(...) printf("[ERROR] " __VA_ARGS__)
四、 多行宏定义:行连接符 \
1. 语法与功能
当宏的替换文本过长,需要跨多行编写时,在每行末尾 添加 \(行连接符),预处理阶段会忽略 \ 和换行符,将多行合并为一行。
2. 注意事项
\后面不能有任何字符(包括空格、制表符),否则会编译错误- 最后一行无需添加
\
3. 示例(对应你代码中的ADC_TAG_LIST)
// 多行宏,集中管理ADC数据
#define ADC_TAG_LIST(TAG) \
TAG(Call1, 1715, CallKeyHandle) \
TAG(Call2, 0, CallKeyHandle) \
TAG(Call3, 560, CallKeyHandle) \
TAG(Call4, 2220, CallKeyHandle) \
TAG(Exit1, 870, ExitBtnHandle) \
TAG(Exit2, 1170, ExitBtnHandle)
// 基础示例:多行运算宏
#define COMPUTE(a, b, c) \
((a) * (b) + (c)) + \
((a) + (b) * (c))
五、 宏嵌套:宏参数为宏
1. 语法与功能
宏的参数可以是另一个宏的调用,预处理阶段会逐层展开(先展开参数,再展开外层宏),是「一份数据,多用途复用」的核心技巧(对应你代码的核心设计)。
2. 示例(对应你代码中的AdcVoltageGroup)
// 步骤1:定义两个用途不同的宏
#define DEFINE_ADC_TAG(_TAG, ADC, FUNC) _TAG,
#define DEFINE_ADC_VOLTAGE(_TAG, ADC, FUNC) {#_TAG, ADC, FUNC},
// 步骤2:定义数据仓库宏(多行宏)
#define ADC_TAG_LIST(TAG) \
TAG(Call1, 1715, CallKeyHandle) \
TAG(Call2, 0, CallKeyHandle)
// 步骤3:宏嵌套调用,生成不同数组
// 生成标签名数组
char* AdcTagArray[] = {ADC_TAG_LIST(DEFINE_ADC_TAG)};
// 展开后:char* AdcTagArray[] = {Call1, Call2,};
// 生成ADC电压结构体数组(你的代码核心)
static AdcVoltage AdcVoltageGroup[] = {ADC_TAG_LIST(DEFINE_ADC_VOLTAGE)};
// 展开后:static AdcVoltage AdcVoltageGroup[] = {{"Call1",1715,CallKeyHandle}, {"Call2",0,CallKeyHandle},};
六、 条件编译宏:控制代码编译流程
条件编译宏用于根据不同场景(调试 / 发布、跨平台、硬件配置)选择性编译代码,核心指令包括#ifdef、#ifndef、#if、#elif、#else、#endif、#undef。
1. 常用指令说明
| 指令 | 功能说明 |
|---|---|
#ifdef 宏名 |
判断宏是否已定义(定义过则编译后续代码) |
#ifndef 宏名 |
判断宏是否未定义(未定义则编译后续代码) |
#if 常量表达式 |
判断常量表达式是否为真(非 0 为真) |
#elif 常量表达式 |
#if的分支判断(类似 else if) |
#else |
条件不满足时的分支(类似 else) |
#endif |
结束条件编译块(必须配对使用) |
#undef 宏名 |
取消已定义的宏(释放宏名) |
2. 实用示例
(1) 调试模式与发布模式切换
#define DEBUG 1 // 1=开启调试,0=关闭调试
#ifdef DEBUG
#define LOG_DEBUG(format, ...) printf("[DEBUG][%s:%d] " format "\n", __FILE__, __LINE__, ##__VA_ARGS__)
#else
#define LOG_DEBUG(format, ...)
#endif
// 使用:调试模式打印文件名+行号,发布模式无输出
LOG_DEBUG("GPIO引脚%d初始化完成", 60);
(2) 跨平台兼容
#define OS_LINUX 1
#if OS_LINUX
#include <unistd.h>
#define DELAY_US(x) usleep(x)
#elif OS_WINDOWS
#include <windows.h>
#define DELAY_US(x) Sleep(x / 1000)
#else
#error "未定义操作系统类型"
#endif
(3) 取消宏定义
#define MAX_NUM 100
printf("MAX_NUM: %d\n", MAX_NUM); // 输出100
#undef MAX_NUM // 取消宏定义
#define MAX_NUM 200
printf("MAX_NUM: %d\n", MAX_NUM); // 输出200
七、 预定义宏:编译器自带的内置宏
C 语言编译器提供一系列预定义宏(无需手动#define),常用于日志打印、调试定位,核心预定义宏如下:
| 预定义宏 | 功能说明 |
|---|---|
__FILE__ |
当前源文件名(字符串常量) |
__LINE__ |
当前代码行号(整数常量) |
__FUNCTION__ |
当前函数名(字符串常量,GCC 特有) |
__DATE__ |
编译日期(格式:"MMM DD YYYY") |
__TIME__ |
编译时间(格式:"HH:MM:SS") |
__STDC__ |
若为 1,表示遵循 ANSI C 标准 |
示例
void AdcErrorHandle(int pin) {
// 打印错误位置、时间
printf("[ERROR] %s %s:%d - GPIO%d 配置失败\n",
__DATE__, __FUNCTION__, __LINE__, pin);
}
八、 宏的优缺点与使用规范
1. 优点
- 无运行时开销:预处理阶段完成替换,不产生函数调用栈,执行效率高
- 代码复用:批量复用代码片段,减少冗余
- 提高可维护性:常量集中定义,修改时只需改宏定义,无需逐行修改
- 灵活扩展:支持条件编译、参数化替换,适配不同场景
2. 缺点
- 代码膨胀:纯文本替换可能导致最终编译的代码体积增大
- 无类型检查:宏参数不做类型校验,容易出现类型不匹配错误
- 调试困难:预处理后宏已展开,调试时无法直接查看宏的原始形态
- 易产生隐藏错误:运算优先级、副作用等问题不易排查
3. 使用规范
- 宏名大写:区分普通变量 / 函数(如
MAX_NUM、ADC_TAG_LIST) - 复杂宏加括号:无参数宏整体加括号,带参数宏实现「参数 + 整体」双重括号
- 避免副作用:参数避免使用
i++、--j、函数调用等有副作用的表达式 - 精简宏逻辑:复杂功能优先用函数实现,宏仅用于简单复用 / 常量定义
- 注释清晰:对批量配置、复杂宏添加注释,说明用途和参数含义
- 避免多重定义:同一宏名仅定义一次,必要时用
#undef取消后重新定义
总结
- 宏分为「无参数宏」(常量定义)和「带参数宏」(代码片段复用),核心是「纯文本替换」。
- 高级特性:
#(字符串化)、##(拼接)、.../__VA_ARGS__(可变参数)、\(多行连接)、宏嵌套是嵌入式开发的核心技巧。 - 条件编译宏(
#ifdef/#if)用于控制代码编译流程,预定义宏(__FILE__/__LINE__)用于调试定位。 - 使用宏时需规避「优先级问题」「副作用」「代码膨胀」等坑,遵循「大写命名、加括号、精简逻辑」的规范。
- 你提供的 ADC 配置宏(
ADC_TAG_LIST/DEFINE_ADC_VOLTAGE)是宏嵌套 + 多行宏的经典应用,实现了「一份数据,多用途复用」。