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() 条件编译的核心逻辑:
从这张图可以看出,defined() 的核心价值在于「组合」。单条件用 #ifdef 就够了,但一旦涉及两个以上条件的逻辑运算,defined() 就是最优雅的解法。
好了,关于 #if defined() 和 #if !defined() 的内容就讲到这里。记住一句话:条件编译不是炫技,是为了让代码在不同环境下都能正确运行。用最清晰的方式表达你的意图,比用最「高级」的写法重要得多。
公众号:蓝海资料掘金营,微信deep3321