7、#elif与#else:多分支条件编译,复杂条件表达式(defined() 运算符),实战:跨平台代码管理
条件编译这东西,说白了就是让编译器帮你“挑着看代码”。#if、#elif、#else 这三个家伙,跟 C 语言里的 if-else if-else 结构很像,但作用对象是代码块本身。我刚开始学的时候总觉得这玩意儿可有可无,直到有一次接手一个跨平台项目……嗯,从那以后我再也不敢小看它了。
7.1 多分支条件编译的基本语法
先看个最简单的例子。假设我们要根据不同的调试等级输出不同信息:
#define DEBUG_LEVEL 2
#if DEBUG_LEVEL == 0
#define LOG(msg) // 啥也不做
#elif DEBUG_LEVEL == 1
#define LOG(msg) printf("[INFO] %s\n", msg)
#elif DEBUG_LEVEL == 2
#define LOG(msg) printf("[DEBUG] %s, line %d\n", msg, __LINE__)
#else
#define LOG(msg) printf("[VERBOSE] %s, func: %s\n", msg, __func__)
#endif
你看,一个 #if 开头,中间可以跟任意多个 #elif,最后用 #else 兜底,#endif 收尾。编译器在预处理阶段就会根据 DEBUG_LEVEL 的值,只保留匹配的那个分支,其余的全部扔掉。
7.2 defined() 运算符:检查宏是否被定义
有时候我们关心的不是宏的值,而是它“有没有被定义”。这时候 defined() 就派上用场了。它返回 1(已定义)或 0(未定义)。
#if defined(__linux__)
#include <unistd.h>
#define PLATFORM "Linux"
#elif defined(_WIN32)
#include <windows.h>
#define PLATFORM "Windows"
#elif defined(__APPLE__)
#include <TargetConditionals.h>
#define PLATFORM "macOS"
#else
#define PLATFORM "Unknown"
#warning "Unknown platform, using generic settings"
#endif
我个人习惯把 defined() 和逻辑运算符组合使用,比如:
#if defined(DEBUG) && defined(VERBOSE)
#define TRACE(fmt, ...) printf("[TRACE] " fmt "\n", ##__VA_ARGS__)
#elif defined(DEBUG)
#define TRACE(fmt, ...) // 空定义
#else
#define TRACE(fmt, ...) // 空定义
#endif
这里有个坑——defined() 只能用在 #if 或 #elif 后面,不能用在 #ifdef 或 #ifndef 里。你想想看,#ifdef 本身就是 #if defined(...) 的语法糖,没必要再套一层。
7.3 复杂条件表达式
条件编译支持 &&、||、! 这些逻辑运算符,也支持 ==、!=、<、> 等比较运算符。甚至可以用括号改变优先级:
#define FEATURE_A 1
#define FEATURE_B 0
#define PLATFORM_ID 2
#if (defined(FEATURE_A) && FEATURE_A == 1) || \
(defined(PLATFORM_ID) && PLATFORM_ID >= 2)
// 启用高级功能
void advanced_init(void);
#endif
注意反斜杠 \ 是用来换行的。预处理指令本身是一行,太长的话可以用反斜杠续行。我曾经见过有人把十几个条件写在一行里,那代码简直没法看……
#if UNDEFINED_MACRO 等价于 #if 0,这个分支会被跳过。这既是特性也是陷阱——拼写错误可能导致静默跳过代码。
7.4 实战:跨平台代码管理
好了,理论说完了,咱们来点实际的。跨平台代码管理是 #elif 和 defined() 最经典的应用场景。我在做嵌入式项目时,经常需要一套代码同时支持 STM32、ESP32 和 Linux 模拟器。
下面是一个简化版的跨平台内存管理模块:
// mem_manager.h
#ifndef MEM_MANAGER_H
#define MEM_MANAGER_H
#include <stddef.h>
// 平台检测
#if defined(STM32F4)
#include "stm32f4xx_hal.h"
#define HEAP_SIZE (64 * 1024) // 64KB
#define ALIGNMENT 8
#elif defined(ESP32)
#include "esp_heap_caps.h"
#define HEAP_SIZE (256 * 1024) // 256KB
#define ALIGNMENT 4
#elif defined(__linux__)
#include <stdlib.h>
#define HEAP_SIZE (1024 * 1024) // 1MB
#define ALIGNMENT 16
#else
#error "Unsupported platform! Define STM32F4, ESP32, or __linux__"
#endif
// 内存分配函数
void* my_malloc(size_t size);
void my_free(void* ptr);
#endif // MEM_MANAGER_H
对应的实现文件:
// mem_manager.c
#include "mem_manager.h"
static char heap[HEAP_SIZE];
static size_t heap_index = 0;
void* my_malloc(size_t size) {
// 对齐处理
size = (size + ALIGNMENT - 1) & ~(ALIGNMENT - 1);
if (heap_index + size > HEAP_SIZE) {
return NULL;
}
void* ptr = &heap[heap_index];
heap_index += size;
#if defined(STM32F4)
// STM32 需要清空缓存
SCB_CleanInvalidateDCache();
#elif defined(ESP32)
// ESP32 需要确保内存在 DMA 区域
if ((uint32_t)ptr & 0x3) {
// 地址未对齐,记录警告
}
#endif
return ptr;
}
void my_free(void* ptr) {
// 简化实现:不支持释放
(void)ptr;
}
你看,通过 #if-#elif-#else,我们把平台相关的代码隔离得干干净净。添加新平台时,只需要在头文件里加一个分支,其他地方基本不用动。
7.5 知识体系图
下面这张图总结了本章的核心逻辑:
7.6 避坑指南
最后分享几个我踩过的坑:
- 宏定义拼写错误:我曾经把
STM32F4写成STM32F4(少了个下划线),结果编译器默默走了#else分支,导致整个项目链接失败。排查了整整一下午…… - 忘记 #endif:每个
#if都必须有对应的#endif。嵌套使用时尤其容易漏掉。我习惯在#endif后面加注释:#endif /* PLATFORM_CHECK */ - defined() 与 #ifdef 混用:
#ifdef MACRO等价于#if defined(MACRO),但#ifdef不支持&&和||。需要组合条件时,老老实实用#if defined()。 - 条件表达式中的宏展开:
#if VALUE == 2要求VALUE在预处理阶段就能展开成整数常量。如果VALUE是sizeof(int)这种运行时值,编译器会报错。
#elif 和 defined(),能让你的代码在多个平台间优雅切换。记住——条件编译不是用来隐藏代码的,而是用来管理差异的。
公众号:蓝海资料掘金营,微信deep3321