13、#if defined() 与 #if !defined():多条件组合、复杂条件判断

预处理条件编译,说白了就是让编译器「看人下菜碟」。#if defined()#if !defined() 就是这桌菜里最灵活的两把勺子。我个人习惯把它们叫做「存在性检查器」——检查某个宏到底有没有被定义,而不关心它的值是多少。

你可能会问:#ifdef 不是也能干这事吗?没错,但 defined() 的优势在于它可以跟 &&||! 自由组合。这就厉害了,相当于从单条件开关升级成了多条件逻辑引擎。

13.1 基本用法:defined() 与 !defined()

先看最基础的写法:

#if defined(DEBUG)
    printf("调试模式已开启\n");
#endif

#if !defined(NDEBUG)
    printf("非发布版本,保留断言\n");
#endif

这两段代码等价于:

#ifdef DEBUG
    printf("调试模式已开启\n");
#endif

#ifndef NDEBUG
    printf("非发布版本,保留断言\n");
#endif

嗯,这里要注意:defined() 是预处理运算符,不是函数。它只关心宏「有没有被定义」,不关心宏的值是 0 还是 1。哪怕你写了 #define DEBUG 0#if defined(DEBUG) 依然为真。

我曾经踩过这个坑:有个同事用 #define ENABLE_FEATURE 0 来「关闭」某个功能,结果 #ifdef ENABLE_FEATURE 依然编译了那段代码。正确的做法是用 #if ENABLE_FEATURE 或者直接 #undef ENABLE_FEATURE

13.2 多条件组合:逻辑与、逻辑或

这才是 defined() 的真正用武之地。单条件用 #ifdef 就够了,但多条件组合必须上 defined()

13.2.1 逻辑与(&&)

假设我们有一个嵌入式项目,需要同时满足「硬件平台是 STM32」且「启用了 RTOS」才编译某段代码:

#if defined(PLATFORM_STM32) && defined(ENABLE_RTOS)
    osThreadDef(defaultTask, TaskFunction, osPriorityNormal, 0, 128);
    osThreadCreate(osThread(defaultTask), NULL);
#endif

这种写法比嵌套 #ifdef 清爽多了。嵌套写法是这样的:

#ifdef PLATFORM_STM32
    #ifdef ENABLE_RTOS
        // 代码
    #endif
#endif

你想想看,如果条件增加到三四个,嵌套层数会变得非常难看。用 defined() 组合,一行搞定。

13.2.2 逻辑或(||)

我在项目中遇到过这样的场景:某个驱动需要兼容两种不同的传感器型号,只要定义了其中任意一个,就编译驱动代码:

#if defined(SENSOR_TYPE_A) || defined(SENSOR_TYPE_B)
    #include "sensor_driver.h"
    void sensor_init(void) {
        #if defined(SENSOR_TYPE_A)
            sensor_a_init();
        #endif
        #if defined(SENSOR_TYPE_B)
            sensor_b_init();
        #endif
    }
#endif

这样写的好处是:新增传感器类型 C 时,只需要在条件里加一个 || defined(SENSOR_TYPE_C),不用改任何内部逻辑。

13.2.3 逻辑非(!)

!defined() 常用于「排除法」。比如我们想在所有非 Windows 平台上启用某个 POSIX 特性:

#if !defined(_WIN32) && !defined(_WIN64)
    #include <unistd.h>
    #define sleep_ms(ms) usleep((ms) * 1000)
#endif

这种写法比 #ifndef _WIN32 更灵活,因为你可以继续叠加其他条件。

13.3 复杂条件判断:三条件以上组合

实际项目中,条件判断往往不止两三个。我整理了一个真实案例:某物联网设备需要根据硬件版本、网络协议、安全等级三个维度来决定编译哪套通信栈。

#if (defined(HW_VERSION_2) || defined(HW_VERSION_3)) && \
    defined(NET_PROTOCOL_MQTT) && \
    !defined(DISABLE_TLS)
    #include "mqtt_tls_transport.h"
    #define TRANSPORT_INIT() mqtt_tls_init()
#elif defined(HW_VERSION_1) && defined(NET_PROTOCOL_MQTT)
    #include "mqtt_plain_transport.h"
    #define TRANSPORT_INIT() mqtt_plain_init()
#elif defined(NET_PROTOCOL_COAP)
    #include "coap_transport.h"
    #define TRANSPORT_INIT() coap_init()
#else
    #error "No valid transport configuration found!"
#endif

这里用了反斜杠 \ 做续行,让长条件更易读。我个人习惯把 &&|| 写在行首,这样一眼就能看出逻辑关系。

核心原则:复杂条件判断时,优先用 defined() 组合,而不是嵌套 #ifdef。前者是「平面逻辑」,后者是「树形逻辑」——平面逻辑更容易维护。

13.4 实战技巧:条件编译的层次设计

我见过不少项目,条件编译写得像一团乱麻。后来我总结了一套分层设计方法,分享给你:

层次 用途 示例
第一层 平台/架构选择 #if defined(__ARM_ARCH)
第二层 功能模块开关 #if defined(ENABLE_BLUETOOTH)
第三层 调试/发布模式 #if !defined(NDEBUG)
第四层 具体特性细调 #if defined(USE_FAST_MATH)

这种分层的好处是:每个条件只关注一个维度,组合时逻辑清晰。我曾经在一个项目里看到有人把平台判断和功能开关混在一起写,结果改一个平台要改七八个地方——那叫一个酸爽。

13.5 避坑指南:defined() 的常见误区

defined() 这么多年,我总结了几条血泪教训:

  • 别把 defined() 当函数调用:它后面跟的是宏名,不是表达式。#if defined(1+2) 是错的。
  • 注意括号位置:#if defined(MACRO)#if defined MACRO 都合法,但前者更清晰。我建议统一加括号。
  • 小心宏被 #undef:如果某个头文件里 #undef 了你的宏,后面的 defined() 会返回假。排查时很难发现。
  • 别在 defined() 里用复杂表达式:比如 #if defined(A) && defined(B) || !defined(C),这种逻辑最好加括号明确优先级:#if (defined(A) && defined(B)) || !defined(C)
一个小技巧:如果你不确定某个宏是否被定义,可以在代码里临时加一行 #if defined(YOUR_MACRO) #error "YOUR_MACRO is defined" #endif,编译时看报错信息就知道结果了。

13.6 知识体系图

下面这张图梳理了 defined() 条件编译的核心逻辑:

#if defined() 条件编译体系 defined() 运算符 单条件:defined(X) 等价于 #ifdef X 多条件:&& || ! 组合 #if defined(A) && !defined(B) || defined(C) 取反:!defined(X) 等价于 #ifndef X 优势:平面逻辑、灵活组合、可读性强 注意:只检查存在性,不关心宏的值

从这张图可以看出,defined() 的核心价值在于「组合」。单条件用 #ifdef 就够了,但一旦涉及两个以上条件的逻辑运算,defined() 就是最优雅的解法。

好了,关于 #if defined()#if !defined() 的内容就讲到这里。记住一句话:条件编译不是炫技,是为了让代码在不同环境下都能正确运行。用最清晰的方式表达你的意图,比用最「高级」的写法重要得多。


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