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 知识体系总览

下面这张图,是我对本章知识点的梳理。你可以把它当作一张「跨平台宏定义作战地图」:

预处理与宏:跨平台开发知识体系 语法基础 • #ifdef / #ifndef • #elif defined() • #endif / #else • #error 编译期断言 平台检测宏 • _WIN32 / _WIN64 • __linux__ • __APPLE__ / __unix__ • __GNUC__ / __clang__ 实战技巧 • 跨平台日志库 • 颜色输出适配 • 线程安全时间函数 • 可变参数宏封装 ⚠️ 避坑指南(我踩过的坑) 1. 宏展开副作用:参数和表达式务必加括号 ((x)*(x)) 2. 多语句宏用 do { ... } while(0) 包裹 3. 需要类型安全时,用 static inline 替代宏

3.6 总结:跨平台宏定义的核心原则

说了这么多,其实就三条原则:

  1. 先检测,后适配:用平台检测宏判断环境,再写对应的代码分支
  2. 封装变化:把平台差异封装到宏或内联函数里,上层代码保持统一
  3. 编译期报错优于运行时崩溃:用 #error 在编译时就暴露不支持的平台

我个人习惯在项目根目录放一个 platform.h,把所有平台检测和适配宏集中管理。这样新成员加入时,只需要看这一个文件就能了解项目的跨平台策略。嗯,这个做法推荐给你。

最后一个小建议: 别滥用宏。能用 const 就别用 #define 定义常量,能用 enum 就别用宏定义枚举。宏是跨平台利器,但也是双刃剑——用对了事半功倍,用错了追悔莫及。

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