C 语言宏定义(#define)语法与用法大全

宏定义是 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) 必做优化:参数 + 整体双重加括号

带参数宏的替换易受运算优先级影响,必须满足两个要求:

  1. 每个参数单独用括号包裹(避免参数本身是表达式时出错)

  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. 使用规范

  1. 宏名大写:区分普通变量 / 函数(如MAX_NUMADC_TAG_LIST
  2. 复杂宏加括号:无参数宏整体加括号,带参数宏实现「参数 + 整体」双重括号
  3. 避免副作用:参数避免使用i++--j、函数调用等有副作用的表达式
  4. 精简宏逻辑:复杂功能优先用函数实现,宏仅用于简单复用 / 常量定义
  5. 注释清晰:对批量配置、复杂宏添加注释,说明用途和参数含义
  6. 避免多重定义:同一宏名仅定义一次,必要时用#undef取消后重新定义

总结

  1. 宏分为「无参数宏」(常量定义)和「带参数宏」(代码片段复用),核心是「纯文本替换」。
  2. 高级特性:#(字符串化)、##(拼接)、.../__VA_ARGS__(可变参数)、\(多行连接)、宏嵌套是嵌入式开发的核心技巧。
  3. 条件编译宏(#ifdef/#if)用于控制代码编译流程,预定义宏(__FILE__/__LINE__)用于调试定位。
  4. 使用宏时需规避「优先级问题」「副作用」「代码膨胀」等坑,遵循「大写命名、加括号、精简逻辑」的规范。
  5. 你提供的 ADC 配置宏(ADC_TAG_LIST/DEFINE_ADC_VOLTAGE)是宏嵌套 + 多行宏的经典应用,实现了「一份数据,多用途复用」。
相关推荐
知南x5 小时前
【Ascend C系列课程(高级)】(1) 算子调试+调优
c语言·开发语言
2的n次方_7 小时前
Runtime 执行提交机制:NPU 硬件队列的管理与任务原子化下发
c语言·开发语言
凡人叶枫7 小时前
C++中智能指针详解(Linux实战版)| 彻底解决内存泄漏,新手也能吃透
java·linux·c语言·开发语言·c++·嵌入式开发
凡人叶枫9 小时前
C++中输入、输出和文件操作详解(Linux实战版)| 从基础到项目落地,避坑指南
linux·服务器·c语言·开发语言·c++
CODECOLLECT9 小时前
京元 I62D Windows PDA 技术拆解:Windows 10 IoT 兼容 + 硬解码模块,如何降低工业软件迁移成本?
stm32·单片机·嵌入式硬件
BackCatK Chen10 小时前
STM32+FreeRTOS:嵌入式开发的黄金搭档,未来十年就靠它了!
stm32·单片机·嵌入式硬件·freertos·低功耗·rtdbs·工业控制
傻乐u兔10 小时前
C语言进阶————指针3
c语言·开发语言
CodeSheep程序羊11 小时前
拼多多春节加班工资曝光,没几个敢给这个数的。
java·c语言·开发语言·c++·python·程序人生·职场和发展
I'mChloe11 小时前
PTO-ISA 深度解析:PyPTO 范式生成的底层指令集与 NPU 算子执行的硬件映射
c语言·开发语言
2的n次方_12 小时前
Runtime 内存管理深化:推理批处理下的内存复用与生命周期精细控制
c语言·网络·架构