3、预处理与宏:跨平台宏定义的艺术
预处理,说白了就是编译前的文本替换。很多新手觉得这玩意儿就是简单的「找找替换」,其实不然。我在跨平台项目里摸爬滚打这么多年,可以负责任地告诉你:预处理与宏,是跨平台移植的第一道防线。
你想想看,Windows 上用 _WIN32,Linux 上用 __linux__,macOS 上用 __APPLE__——这些平台检测宏,就是你的代码在不同操作系统间「说人话」的翻译官。没有它们,你的代码要么只能在一种平台上跑,要么就得写一堆丑陋的运行时判断。
3.1 跨平台宏定义的核心语法
先看最基础的三件套:#ifdef、#ifndef、#endif。这三个指令,我几乎每天都在用。
#ifdef PLATFORM_WINDOWS
// Windows 专用代码
#include <windows.h>
#elif defined(PLATFORM_LINUX)
// Linux 专用代码
#include <unistd.h>
#else
#error "Unknown platform!"
#endif
这里有个细节:#elif defined() 是 #elifdef 的等价写法。我个人习惯用 defined(),因为可读性更好,一眼就能看出是在检查宏是否定义。
#if defined() 而不是 #ifdef。为什么?因为 defined() 支持逻辑运算:#if defined(A) && !defined(B),而 #ifdef 做不到。
3.2 平台检测宏:编译器给你铺好的路
每个编译器都会预定义一些宏,告诉你当前在什么平台上。这些宏是跨平台开发的基石。我整理了一份常用列表:
| 宏名称 | 平台/编译器 | 说明 |
|---|---|---|
_WIN32 |
Windows (32/64位) | MSVC、MinGW 都定义 |
__linux__ |
Linux | GCC、Clang 都定义 |
__APPLE__ |
macOS / iOS | 包括 __MACH__ |
__unix__ |
类 Unix 系统 | Linux、macOS 也定义 |
__ANDROID__ |
Android | NDK 环境下定义 |
__clang__ |
Clang 编译器 | 检测编译器类型 |
__GNUC__ |
GCC 编译器 | 包括 MinGW |
_WIN32 在 64 位 Windows 上也是定义的。别被名字骗了,它不代表 32 位系统。如果你想区分 x86 和 x64,得用 _WIN64。
3.3 条件编译实战:一个跨平台日志库
光说不练假把式。我们写一个实际可用的跨平台日志模块,看看这些宏怎么配合使用。
// logger.h
#ifndef LOGGER_H
#define LOGGER_H
#include <stdio.h>
#include <stdarg.h>
#include <time.h>
// 日志级别
typedef enum {
LOG_DEBUG,
LOG_INFO,
LOG_WARN,
LOG_ERROR
} LogLevel;
// 跨平台的颜色输出
#if defined(_WIN32)
#include <windows.h>
// Windows 下用 SetConsoleTextAttribute
#define LOG_COLOR_RED 12
#define LOG_COLOR_GREEN 10
#define LOG_COLOR_YELLOW 14
#define LOG_COLOR_DEFAULT 7
#elif defined(__linux__) || defined(__APPLE__)
// Unix 下用 ANSI 转义序列
#define LOG_COLOR_RED "\033[31m"
#define LOG_COLOR_GREEN "\033[32m"
#define LOG_COLOR_YELLOW "\033[33m"
#define LOG_COLOR_DEFAULT "\033[0m"
#else
#error "Unsupported platform"
#endif
// 获取颜色
static const char* log_level_color(LogLevel level) {
switch(level) {
case LOG_DEBUG: return LOG_COLOR_GREEN;
case LOG_INFO: return LOG_COLOR_DEFAULT;
case LOG_WARN: return LOG_COLOR_YELLOW;
case LOG_ERROR: return LOG_COLOR_RED;
default: return LOG_COLOR_DEFAULT;
}
}
// 核心日志函数
void log_message(LogLevel level, const char* file, int line,
const char* fmt, ...) {
time_t now = time(NULL);
struct tm* tm_info;
// 跨平台的时间获取
#if defined(_WIN32)
struct tm tm_buf;
localtime_s(&tm_buf, &now);
tm_info = &tm_buf;
#else
struct tm tm_buf;
localtime_r(&now, &tm_buf);
tm_info = &tm_buf;
#endif
char time_buf[20];
strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", tm_info);
// 输出带颜色的日志
#if defined(_WIN32)
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(hConsole, log_level_color(level));
#else
printf("%s", log_level_color(level));
#endif
printf("[%s] [%s:%d] ", time_buf, file, line);
va_list args;
va_start(args, fmt);
vprintf(fmt, args);
va_end(args);
// 恢复默认颜色
#if defined(_WIN32)
SetConsoleTextAttribute(hConsole, LOG_COLOR_DEFAULT);
#else
printf("%s", LOG_COLOR_DEFAULT);
#endif
printf("\n");
}
// 便捷宏
#define LOG_DEBUG(fmt, ...) \
log_message(LOG_DEBUG, __FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_INFO(fmt, ...) \
log_message(LOG_INFO, __FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_WARN(fmt, ...) \
log_message(LOG_WARN, __FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_ERROR(fmt, ...) \
log_message(LOG_ERROR, __FILE__, __LINE__, fmt, ##__VA_ARGS__)
#endif // LOGGER_H
这个例子涵盖了三个关键点:
- 头文件保护:
#ifndef LOGGER_H防止重复包含 - 平台差异封装:颜色输出、线程安全的时间函数,都用了条件编译
- 可变参数宏:
##__VA_ARGS__是 GNU 扩展,MSVC 也支持
3.4 避坑指南:我踩过的那些宏的坑
嗯,这里要重点说说。宏虽然好用,但坑也不少。我一个个说:
坑一:宏展开的副作用
#define SQUARE(x) x * x
// 调用:SQUARE(a + b) → a + b * a + b ❌
// 正确写法:#define SQUARE(x) ((x) * (x))
我曾经在计算缓冲区大小时用过类似的宏,结果线上出了个诡异的 bug,排查了两天才发现是宏展开的问题。从那以后,我写带参数的宏,每个参数和整个表达式都加括号。
坑二:分号问题
#define DO_SOMETHING() do { \
func1(); \
func2(); \
} while(0)
这个 do { ... } while(0) 写法,业内叫「do-while-zero 惯用法」。它保证了宏在使用时加不加分号都不会出问题。我建议所有多语句宏都这么写。
坑三:宏与类型安全
宏不检查类型。如果你需要类型安全,考虑用 static inline 函数替代。C99 以后的内联函数,在大多数情况下性能不输宏,但安全得多。
3.5 知识体系总览
下面这张图,是我对本章知识点的梳理。你可以把它当作一张「跨平台宏定义作战地图」:
3.6 总结:跨平台宏定义的核心原则
说了这么多,其实就三条原则:
- 先检测,后适配:用平台检测宏判断环境,再写对应的代码分支
- 封装变化:把平台差异封装到宏或内联函数里,上层代码保持统一
- 编译期报错优于运行时崩溃:用
#error在编译时就暴露不支持的平台
我个人习惯在项目根目录放一个 platform.h,把所有平台检测和适配宏集中管理。这样新成员加入时,只需要看这一个文件就能了解项目的跨平台策略。嗯,这个做法推荐给你。
const 就别用 #define 定义常量,能用 enum 就别用宏定义枚举。宏是跨平台利器,但也是双刃剑——用对了事半功倍,用错了追悔莫及。
公众号:蓝海资料掘金营,微信deep3321