预处理

C 语言预处理完全指南

预处理是 C 编译流程的第一步,负责宏展开、条件编译和文件包含。掌握预处理技术可以实现代码复用、跨平台兼容与调试开关。本文详细介绍预处理指令的用法与技巧。

一、预处理流程概述

C 代码编译分为四个阶段:预处理、编译、汇编、链接。预处理阶段完成以下工作:

  • 展开宏定义(#define)
  • 处理文件包含(#include)
  • 条件编译(#if、#ifdef、#ifndef)
  • 处理特殊指令(#pragma、#error)
/* 查看预处理后的代码 */
$ gcc -E main.c -o main.i  /* 只预处理,不编译 */
$ cpp main.c > main.i      /* 手动调用预处理器 */

二、宏定义详解

1. 简单宏

/* 符号常量 */
#define PI 3.14159
#define MAX_SIZE 1024
#define VERSION "1.0.0"

2. 带参宏

/* 计算两个数的最大值 */
#define MAX(a, b) ((a) > (b) ? (a) : (b))

/* 宏的常见错误:漏加括号 */
#define SQUARE(x) x * x   /* 错误:SQUARE(1+1) 变成 1+1*1+1 = 3 */
#define SQUARE(x) ((x) * (x))  /* 正确 */

/* 多语句宏,用 do {...} while(0) 包装 */
#define SET_ARRAY(ptr, val, n) \
    do { \
        for (int i = 0; i < (n); i++) (ptr)[i] = (val); \
    } while(0)

3. 字符串化与连接

/* #x 将参数转为字符串 */
#define PRINT_INT(x) printf(#x " = %d\n", x)
PRINT_INT(i + j);  /* 输出: i + j = 123 */

/* ## 连接标识符 */
#define GENERATE_FUNC(name) int func_##name() { return 0; }
GENERATE_FUNC(test);  /* 生成 func_test() */

三、条件编译

/* 跨平台兼容 */
#if defined(__linux__)
    /* Linux 平台代码 */
    #include 
#elif defined(_WIN32)
    /* Windows 平台代码 */
    #include 
#else
    /* 其他平台 */
#endif
/* 调试开关 */
#define DEBUG  /* 打开调试模式,删除这行则关闭 */

#ifdef DEBUG
    printf("Debug: x = %d\n", x);
#endif

/* 防止重复包含 */
#ifndef HEADER_H
#define HEADER_H
/* 头文件内容 */
#endif

四、预定义宏

/* C 标准预定义宏 */
__FILE__      /* 当前源文件名字符串 */
__LINE__      /* 当前行号整数 */
__DATE__      /* 编译日期字符串 */
__TIME__      /* 编译时间字符串 */
__STDC__      /* C 标准符合标记,值为 1 表示符合 ISO 标准 */
__STDC_VERSION__  /* C 标准版本,如 201710L 表示 C17 */
/* 调试日志宏示例 */
#define LOG(fmt, ...) \
    fprintf(stderr, "[%s:%d] " fmt "\n", \
        __FILE__, __LINE__, ##__VA_ARGS__)

LOG("Error code: %d", errno);

五、#pragma 指令

/* 关闭特定警告 */
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-variable"
int unused_VARiable;  /* 不会产生警告 */
#pragma GCC diagnostic pop

/* 结构体字节对齐 */
#pragma pack(push, 1)
struct Packed { char c; int i; };  /* 5 字节,不用 pack 是 8 字节 */
#pragma pack(pop)

/* 条件编译检查 C 标准 */
#if __STDC_VERSION__ < 201112L
    #error "需要 C11 标准支持"
#endif

六、常见陷阱与最佳实践

宏的常见问题:
  • 宏不是类型安全的,没有类型检查
  • 宏参数要加括号,否则可能出现优先级问题
  • 宏如果包含多语句,要用 do-while 包装
  • 调试时宏难以单步执行

宏 vs inline 函数

/* C99+ 推荐用 inline 函数替代简单宏 */
static inline int max(int a, int b) {
    return a > b ? a : b;
}
inline 函数有类型检查、更安全、调试友好,建议替代简单的数学宏。但宏在条件编译、类型泛型等方面仍有不可替代的优势。

七、实用宏汇总

/* 获取数组大小 */
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))

/* 防止编译器优化( volatile 强制每次从内存读取) */
#define READ_ONCE(x) (*(volatile typeof(x) *)&(x))

/* .container_of - 从成员指针获取结构体指针 */
#define container_of(ptr, type, member) \
    ((type *)((char *)(ptr) - offsetof(type, member)))

八、总结

  • 宏是纯文本替换,在编译前完成展开
  • 带参宏的参数和结果都要加括号,防止优先级问题
  • #ifndef + #define 防止头文件重复包含
  • #ifdef 实现跨平台兼容和调试开关
  • 调试困难时考虑用 inline 函数替代
  • 使用 gcc -E 查看预处理结果