10、条件编译: #ifdef / #ifndef / #if / #elif / #endif 的实际应用

条件编译,说白了就是让编译器“挑着看代码”。

你写了一大堆源码,但有些部分只在特定平台、特定版本、或者调试时才需要。如果每次都手动删改,那简直是灾难。我早年在一个跨平台项目里就吃过这个亏——Windows 和 Linux 的底层接口完全不同,没有条件编译,代码根本没法维护。

10.1 为什么需要条件编译?

嵌入式开发里,条件编译几乎是标配。原因很简单:

  • 硬件差异:同一套代码要跑在 STM32F1 和 STM32F4 上,外设寄存器地址不同。
  • 调试与发布:调试时打印日志,发布时去掉,避免性能损耗。
  • 功能开关:比如低配版不带蓝牙,高配版带蓝牙,用宏控制。
  • 跨平台:Windows 用 Sleep(),Linux 用 usleep()

嗯,这里要注意:条件编译是在预处理阶段完成的,不是运行时。所以它不会增加代码体积,也不会影响执行速度——这是它最大的优势。

核心原则:条件编译只影响“哪些代码被编译”,不影响“代码怎么运行”。

10.2 基本语法与用法

先看最常用的几个指令:

指令 含义 典型场景
#ifdef MACRO 如果定义了 MACRO,则编译后续代码 调试开关、平台判断
#ifndef MACRO 如果没定义 MACRO,则编译后续代码 头文件保护、默认值设置
#if 表达式 如果表达式为真(非0),则编译后续代码 版本号比较、数值判断
#elif 表达式 否则如果表达式为真 多分支选择
#else 否则 默认分支
#endif 结束条件编译块 必须成对出现

举个例子,我在一个传感器驱动里这样用:

#define USE_I2C_DRIVER  1   // 1: 使用I2C, 0: 使用SPI

void sensor_init(void) {
    #if USE_I2C_DRIVER
        i2c_init();
    #else
        spi_init();
    #endif
}

你看,改一个宏定义,底层驱动就换了。不用改函数调用,也不用动业务逻辑。

10.3 #ifdef 与 #if defined() 的区别

很多人觉得 #ifdef#if defined() 是一样的。其实不然。

#ifdef 只能判断“是否定义”,不能判断“定义的值是多少”。而 #if defined() 可以配合逻辑运算符:

#define VERSION 2

#if defined(VERSION) && (VERSION > 1)
    // 版本大于1时才编译
    printf("Advanced feature enabled\n");
#endif

我个人习惯用 #if defined(),因为它更灵活。尤其是需要同时判断多个宏时:

#if defined(DEBUG) && defined(VERBOSE)
    // 调试且详细模式
#endif

小技巧#ifdef 不能写成 #ifdef MACRO && OTHER,但 #if defined(MACRO) && defined(OTHER) 可以。记住这个区别,能省不少调试时间。

10.4 #ifndef 的经典用途:头文件保护

这个你应该很熟悉了。每个头文件几乎都要加:

#ifndef __MY_HEADER_H__
#define __MY_HEADER_H__

// 头文件内容

#endif  // __MY_HEADER_H__

为什么要这样?因为 C 语言不允许同一个结构体、函数声明被定义两次。如果两个 .c 文件都 #include 了同一个头文件,没有保护就会报重复定义错误。

我曾经见过一个项目,头文件嵌套了五六层,结果某个结构体被定义了三次。编译报错信息长达几百行,最后发现就是忘了加 #ifndef。嗯,从那以后我写头文件的第一件事就是加保护。

10.5 #if / #elif / #else 的多分支应用

当你有多个条件需要判断时,#if / #elif / #else 比一堆 #ifdef 嵌套更清晰。

举个例子,我在一个通信协议栈里处理不同平台的内存对齐:

#define PLATFORM_ARM    1
#define PLATFORM_X86    2
#define PLATFORM_RISCV  3

#define CURRENT_PLATFORM  PLATFORM_ARM

#if CURRENT_PLATFORM == PLATFORM_ARM
    #pragma pack(1)
    typedef struct {
        uint8_t  header;
        uint32_t data;
    } __attribute__((packed)) Packet;
#elif CURRENT_PLATFORM == PLATFORM_X86
    #pragma pack(1)
    typedef struct {
        uint8_t  header;
        uint32_t data;
    } Packet;
#else
    #error "Unsupported platform!"
#endif

注意最后那个 #error。如果平台没匹配上,编译直接报错,而不是默默用错误的结构体。这个习惯我强烈推荐——宁可编译失败,也不要运行时崩溃。

10.6 条件编译的避坑指南

我用过不少条件编译,踩过的坑也不少。挑几个典型的说说:

  • 宏定义拼写错误#ifdef DEBUGG 少写一个 G,结果调试代码永远不编译。排查了半天才发现。
  • 忘记 #endif:条件编译块必须成对出现。漏掉一个,后面的代码全乱套。
  • 在 #if 里用变量#if (x > 0) 是不行的,x 必须是宏,不能是变量。预处理阶段变量还没出生呢。
  • 嵌套过深:条件编译嵌套超过三层,可读性急剧下降。我建议最多嵌套两层,再多就拆函数。

警告:不要在 #if 表达式里使用 sizeof 或强制类型转换。预处理阶段不识别这些,结果永远是 0 或报错。

10.7 实战:调试日志的条件编译

嵌入式开发里,调试日志是最常见的条件编译场景。我一般这样设计:

// debug.h
#ifndef __DEBUG_H__
#define __DEBUG_H__

#define DEBUG_LEVEL_NONE    0
#define DEBUG_LEVEL_ERROR   1
#define DEBUG_LEVEL_WARN    2
#define DEBUG_LEVEL_INFO    3

#ifndef DEBUG_LEVEL
    #define DEBUG_LEVEL  DEBUG_LEVEL_ERROR   // 默认只打印错误
#endif

#if DEBUG_LEVEL >= DEBUG_LEVEL_ERROR
    #define LOG_ERROR(fmt, ...)  printf("[ERROR] " fmt "\n", ##__VA_ARGS__)
#else
    #define LOG_ERROR(fmt, ...)  // 空定义,不产生任何代码
#endif

#if DEBUG_LEVEL >= DEBUG_LEVEL_WARN
    #define LOG_WARN(fmt, ...)   printf("[WARN]  " fmt "\n", ##__VA_ARGS__)
#else
    #define LOG_WARN(fmt, ...)
#endif

#if DEBUG_LEVEL >= DEBUG_LEVEL_INFO
    #define LOG_INFO(fmt, ...)   printf("[INFO]  " fmt "\n", ##__VA_ARGS__)
#else
    #define LOG_INFO(fmt, ...)
#endif

#endif  // __DEBUG_H__

使用时,在编译选项里定义 DEBUG_LEVEL

// 调试阶段:打印所有日志
gcc -DDEBUG_LEVEL=3 main.c

// 发布阶段:只打印错误
gcc -DDEBUG_LEVEL=1 main.c

你看,改一个编译参数,日志级别就变了。而且没被编译的日志宏是空定义,不会生成任何代码,也不会影响性能。

10.8 知识体系图

下面这张图总结了条件编译的核心逻辑和常见应用场景:

条件编译知识体系 条件编译 基本指令:#ifdef / #ifndef / #if / #elif / #else / #endif 应用场景:头文件保护 / 平台适配 / 调试开关 / 功能裁剪 注意事项:宏拼写 / #endif配对 / 嵌套深度 / 不能使用变量 最佳实践:默认值 + #error + 分层设计

10.9 总结

条件编译是 C 语言里少有的“编译期决策”工具。用好了,一套代码能适配多个平台、多个版本、多种功能组合。用不好,代码里全是 #ifdef 的迷宫,谁也看不懂。

我个人建议:

  • 能用 #if 就别用 #ifdef,因为 #if 可以判断值,更精确。
  • 条件编译块不要太长,超过 20 行就考虑拆成独立函数。
  • 每个 #if 都要有对应的 #else#error,避免遗漏。
  • 头文件保护是必须的,别偷懒。

记住:条件编译是给编译器看的,不是给人看的。所以写的时候要想着“别人能不能看懂”。


公众号:蓝海资料掘金营,微信deep3321