Skip to content

define

宏定义 (#define) 是 C 语言预处理器提供的一个极其暴力但好用的工具。

如果说 .h 文件是菜单,那宏定义就是 “查找替换” (Ctrl+H)

它的核心机制发生在 编译之前(预处理阶段)。编译器甚至还没开始读你的代码逻辑,预处理器就已经把所有的宏都 替换 成了具体的文本。

我们分三个层级来理解它,从“基础”到“陷阱”。


1. 第一层:给“魔术数字”起名字 (最常用)

在代码里直接写数字(魔术数字)是编程大忌。 比如你写了 delay_ms(72000000 / 1000),别人根本不知道这串数字是干嘛的。

宏定义的作用:

C

#define SYSTEM_CLOCK  72000000
#define DELAY_MS      1000

// 使用时:
delay_ms(SYSTEM_CLOCK / DELAY_MS);

预处理发生了什么? 编译器在真正干活前,会先把代码里的 SYSTEM_CLOCK 全部擦掉,填上 72000000

  • 好处: 修改方便。如果明天主频变成了 100MHz,你只需要改 #define 这一行,全工程自动更新。
  • 注意: 宏定义后面 不需要分号 ;!如果你加了分号,分号也会被替换进代码里,导致报错。

2. 第二层:硬件抽象 (STM32 必备)

在 STM32 开发中,宏定义是硬件移植的神器。

假设你的 LED 接在 PA5 引脚。 如果不使用宏,你的代码到处都是 GPIO_Pin_5。 如果改版了,LED 换到了 PB12,你要在几千行代码里一个个找、一个个改,很容易漏掉。

高手做法:

C

// 在头文件里定义“硬件别名”
#define LED_PORT    GPIOA
#define LED_PIN     GPIO_Pin_5
#define LED_CLK     RCC_APB2Periph_GPIOA

// 在代码里只用别名
void LED_Init() {
    RCC_APB2PeriphClockCmd(LED_CLK, ENABLE); // 开启时钟
    // ... 配置 LED_PIN ...
}

void LED_ON() {
    GPIO_ResetBits(LED_PORT, LED_PIN);
}

好处: 以后换引脚,只需要去头文件里改那三行宏定义,底下的逻辑代码一个字都不用动。


3. 第三层:宏函数 (似是而非的函数)

宏不仅可以定义常量,还可以带参数,长得像函数,但不是函数

C

// 定义一个求平方的宏
#define SQUARE(x)   ((x) * (x))

宏函数 vs 普通函数:

  • 普通函数: 程序运行到这里,会发生跳转(压栈、跳转、执行、返回),有时间开销。
  • 宏函数: 预处理器直接把代码粘贴到这里。速度极快(省去了跳转开销),但代码体积会变大(因为到处都在粘贴)。

☠️ 致命陷阱(面试必问):括号不能省!

如果你偷懒写成: #define SQUARE(x) x * x

当你调用 SQUARE(3 + 2) 时,你以为是 5 * 5 = 25实际上预处理器替换后变成了:3 + 2 * 3 + 2 根据数学优先级(先乘除后加减),结果是 3 + 6 + 2 = 11算错了!

黄金法则: 在写宏函数时,每个参数都要加括号,整个表达式也要加括号。#define SQUARE(x) ((x) * (x))


4. 第四层:条件编译 (代码开关)

这是宏定义的另一个大招:控制哪段代码参与编译

C

#define DEBUG_MODE  1  // 定义一个开关

void main() {
  
    // 这段代码只有在 DEBUG_MODE 定义时才会变黑(参与编译)
    // 否则它是灰色的,编译器当它不存在,不占单片机内存
    #ifdef DEBUG_MODE
        Serial_Printf("System Started...\n");
    #endif

    while(1) { ... }
}

应用场景:

  • 调试开关: 开发时打印日志,量产时关闭宏,日志代码全部消失,节省空间且保护代码。
  • 版本管理: 标准版/专业版共用一套代码,通过宏来控制功能的开启和关闭。

总结

  • #define 是纯文本替换:不要把它当变量,它没有类型检查,也不占内存(除非替换后产生了立即数)。
  • STM32 开发必用:用来给引脚起别名,方便改硬件。
  • 小心宏函数:一定要疯狂加括号 ()
  • 结尾无分号:切记切记。