GCC 完全指南 / 05 - 预处理器详解
05 - 预处理器详解
深入理解 GCC 预处理器的工作原理、宏定义技巧、条件编译和
#include机制。
5.1 预处理器概述
预处理器(Preprocessor)在编译之前运行,负责对源代码进行文本级的变换。它是一个纯粹的文本处理器——不理解 C/C++ 语法,只处理以 # 开头的预处理指令。
源代码 (.c/.cpp)
│
▼
┌──────────────────┐
│ 预处理器 │ ← 文本替换、文件包含、条件编译
│ (cpp) │
└──────┬───────────┘
│
▼
预处理后源代码 (.i/.ii) ← 纯粹的 C/C++ 代码
│
▼
编译器 (cc1/cc1plus)
预处理指令一览
| 指令 | 说明 |
|---|---|
#include | 包含头文件 |
#define | 定义宏 |
#undef | 取消宏定义 |
#if / #ifdef / #ifndef | 条件编译判断 |
#elif / #else | 条件编译分支 |
#endif | 条件编译结束 |
#error | 产生编译错误 |
#warning | 产生编译警告 |
#pragma | 编译器特定指令 |
#line | 修改行号和文件名 |
# | 空指令(无操作) |
5.2 宏定义
对象宏(Object-like Macro)
// 简单常量定义
#define PI 3.14159265358979
#define MAX_BUFFER_SIZE 1024
#define VERSION "1.0.0"
// 带表达式的宏
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
// 空宏(用于条件编译)
#define DEBUG
#define _GNU_SOURCE
函数宏(Function-like Macro)
// 基本函数宏
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define ABS(x) ((x) < 0 ? -(x) : (x))
// 重要:宏参数必须用括号括起来!
#define BAD_MAX(a, b) a > b ? a : b // ❌ 危险
#define GOOD_MAX(a, b) ((a) > (b) ? (a) : (b)) // ✅ 正确
// 展开示例:
// GOOD_MAX(x + 1, y + 2)
// → ((x + 1) > (y + 2) ? (x + 1) : (y + 2)) ✅
// BAD_MAX(x + 1, y + 2)
// → x + 1 > y + 2 ? x + 1 : y + 2 → 运算符优先级问题!
宏的副作用问题
#define SQUARE(x) ((x) * (x))
int a = 5;
int result = SQUARE(a++); // ❌ 危险!
// 展开为: ((a++) * (a++)) → a 被自增两次,行为未定义
// 安全写法:使用内联函数替代
static inline int square(int x) { return x * x; }
多行宏
// 使用反斜杠续行
#define SWAP(a, b) do { \
typeof(a) _tmp = (a); \
(a) = (b); \
(b) = _tmp; \
} while (0)
// do { ... } while(0) 模式确保宏在 if/else 中正确使用
if (condition)
SWAP(x, y); // 正确展开,不会产生悬挂 else 问题
else
other();
可变参数宏(Variadic Macro)
// C99 引入的可变参数宏
#define LOG(fmt, ...) fprintf(stderr, fmt "\n", ##__VA_ARGS__)
#define ERROR(fmt, ...) fprintf(stderr, "[ERROR] " fmt "\n", ##__VA_ARGS__)
#define DEBUG_LOG(fmt, ...) do { \
fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__); \
} while (0)
// 使用
LOG("Hello, %s!", "world");
ERROR("Failed to open file: %s", filename);
DEBUG_LOG("x = %d, y = %d", x, y);
// ##__VA_ARGS__ 的作用:
// 当 __VA_ARGS__ 为空时,## 会去除前面的逗号
LOG("Simple message"); // 展开为 fprintf(stderr, "Simple message\n");
5.3 预定义宏
GCC 提供了大量预定义宏,用于条件编译和调试信息。
标准预定义宏
| 宏 | 说明 | 示例值 |
|---|---|---|
__FILE__ | 当前源文件名 | "main.c" |
__LINE__ | 当前行号 | 42 |
__func__ | 当前函数名(C99) | "main" |
__DATE__ | 编译日期 | "May 10 2026" |
__TIME__ | 编译时间 | "14:30:00" |
__STDC__ | 遵循 C 标准时为 1 | 1 |
__STDC_VERSION__ | C 标准版本号 | 201710L (C17) |
__cplusplus | C++ 标准版本号 | 202002L (C++20) |
GCC 特有预定义宏
| 宏 | 说明 |
|---|---|
__GNUC__ | GCC 主版本号 |
__GNUC_MINOR__ | GCC 次版本号 |
__GNUC_PATCHLEVEL__ | GCC 补丁版本号 |
__OPTIMIZE__ | 使用优化时定义 |
__OPTIMIZE_SIZE__ | 使用 -Os 时定义 |
__x86_64__ | x86-64 架构 |
__aarch64__ | ARM 64-bit 架构 |
__linux__ | Linux 平台 |
__APPLE__ | macOS/iOS 平台 |
_WIN32 | Windows 平台 |
查看所有预定义宏
# 查看所有预定义宏
gcc -dM -E - < /dev/null
# 过滤特定宏
gcc -dM -E - < /dev/null | grep __GNUC__
gcc -dM -E - < /dev/null | grep __x86_64
# C++ 模式
g++ -dM -E -x c++ /dev/null | grep __cplusplus
实用示例:版本信息打印
#include <stdio.h>
// 编译时版本信息
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
void print_version(void) {
printf("Application: %s\n", TOSTRING(APP_NAME));
printf("Version: %s\n", TOSTRING(APP_VERSION));
printf("Compiled: %s %s\n", __DATE__, __TIME__);
printf("Compiler: GCC %d.%d.%d\n",
__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__);
printf("File: %s\n", __FILE__);
#ifdef __linux__
printf("Platform: Linux\n");
#elif defined(__APPLE__)
printf("Platform: macOS\n");
#elif defined(_WIN32)
printf("Platform: Windows\n");
#endif
}
gcc -DAPP_NAME=myapp -DAPP_VERSION=2.1.0 -o hello main.c
5.4 条件编译
基本条件编译
// #ifdef / #ifndef
#ifdef DEBUG
printf("Debug mode\n");
#endif
#ifndef RELEASE
printf("Not release mode\n");
#endif
// #if / #elif / #else
#if defined(__linux__)
#include <unistd.h>
#include <sys/types.h>
#elif defined(_WIN32)
#include <windows.h>
#else
#error "Unsupported platform"
#endif
// #if 可以使用表达式
#if __GNUC__ >= 12
// GCC 12+ 特有的代码
#elif __GNUC__ >= 10
// GCC 10-11 的代码
#else
// 旧版本 GCC
#endif
头文件包含保护(Include Guard)
// myheader.h
#ifndef MYHEADER_H
#define MYHEADER_H
// 头文件内容...
typedef struct {
int x, y;
} Point;
#endif /* MYHEADER_H */
// 现代替代方案(GCC/Clang 特有,非标准)
#pragma once
#pragma once vs Include Guard
| 方式 | 优点 | 缺点 |
|---|---|---|
#ifndef/#define/#endif | 标准 C/C++,所有编译器支持 | 冗长,宏名可能冲突 |
#pragma once | 简洁,编译器可以优化 | 非标准,硬链接场景可能有问题 |
常见条件编译模式
// 模式1:调试日志开关
#ifdef DEBUG
#define DBG_LOG(fmt, ...) fprintf(stderr, "[DBG] " fmt "\n", ##__VA_ARGS__)
#else
#define DBG_LOG(fmt, ...) ((void)0)
#endif
// 模式2:API 导出/导入(Windows DLL)
#ifdef _WIN32
#ifdef MYLIB_EXPORTS
#define MYLIB_API __declspec(dllexport)
#else
#define MYLIB_API __declspec(dllimport)
#endif
#else
#define MYLIB_API __attribute__((visibility("default")))
#endif
MYLIB_API int my_function(int arg);
// 模式3:C/C++ 兼容头文件
#ifndef MYCOMPAT_H
#define MYCOMPAT_H
#ifdef __cplusplus
extern "C" {
#endif
// C 函数声明
int my_function(int arg);
#ifdef __cplusplus
}
#endif
#endif
5.5 #include 深入理解
搜索路径差异
#include <stdio.h> // 系统头文件:搜索系统路径
#include "myheader.h" // 用户头文件:先搜索当前目录,再搜索系统路径
#include 的高级用法
// 包含宏生成的文件名
#define PLATFORM linux
#define PLATFORM_HEADER(str) #str
#include PLATFORM_HEADER(config/linux.h)
// 使用字符串化运算符(有限制,不适用于 #include)
系统头文件与用户头文件的警告差异
# -isystem 将路径设为系统头文件路径(不产生警告)
gcc -isystem /usr/local/include -Wall -o hello main.c
# -I 将路径设为用户头文件路径(会产生警告)
gcc -I/usr/local/include -Wall -o hello main.c
5.6 #error 和 #warning
// 编译时断言
#if !defined(__linux__) && !defined(__APPLE__)
#error "This code only supports Linux and macOS"
#endif
#if __STDC_VERSION__ < 201112L
#error "C11 or later required"
#endif
// 编译警告(GCC 扩展)
#warning "This function is deprecated, use new_function() instead"
使用 _Static_assert(C11)替代部分 #error
// 编译时断言(C11+)
_Static_assert(sizeof(int) >= 4, "int must be at least 4 bytes");
_Static_assert(sizeof(void*) == 8, "64-bit platform required");
// C23 中简化为 static_assert(不带下划线前缀)
static_assert(sizeof(int) >= 4, "int must be at least 4 bytes");
5.7 #pragma 指令
// GCC 诊断 pragma
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-variable"
int unused_var = 42; // 此行不产生未使用变量警告
#pragma GCC diagnostic pop
// 常用的诊断 pragma
#pragma GCC diagnostic warning "-Wall" // 将警告设为警告级别
#pragma GCC diagnostic error "-Wformat" // 将特定警告设为错误级别
// Pack pragma(控制结构体对齐)
#pragma pack(push, 1)
typedef struct {
char a; // 1 byte
int b; // 4 bytes
char c; // 1 byte
} __attribute__((packed)) PackedStruct;
#pragma pack(pop)
// 结构体大小:6 bytes(无填充)
// 不使用 #pragma pack 时可能是 12 bytes
GCC 特有的 #pragma GCC
| Pragma | 说明 |
|---|---|
#pragma GCC optimize ("O2") | 对后续函数设置优化级别 |
#pragma GCC target ("avx2") | 对后续函数设置目标指令集 |
#pragma GCC diagnostic push/pop | 保存/恢复诊断设置 |
#pragma GCC diagnostic ignored "-W..." | 忽略特定警告 |
#pragma GCC poison <name> | 禁止使用特定标识符 |
#pragma GCC dependency "file" | 声明文件依赖 |
// poison 示例:禁止在代码中使用 sprintf
#pragma GCC poison sprintf
// sprintf(buf, "hello"); // 编译错误:poisoned identifier
snprintf(buf, sizeof(buf), "hello"); // OK:使用更安全的版本
5.8 预处理器运算符
字符串化运算符 #
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
printf("%s\n", STRINGIFY(hello world)); // "hello world"
printf("%s\n", TOSTRING(__LINE__)); // "42"(先展开 __LINE__ 为 42,再字符串化)
// 注意:直接 STRINGIFY(__LINE__) 得到 "__LINE__"(不会展开)
// 需要两层宏:TOSTRING → 先展开参数 → STRINGIFY → 字符串化
Token 连接运算符 ##
// 连接两个 token
#define CONCAT(a, b) a ## b
#define VAR_NAME(prefix, n) prefix ## _ ## n
int CONCAT(my, var); // → int myvar;
int VAR_NAME(config, size); // → int config_size;
// 实用示例:自动生成函数名
#define DEFINE_PRINT(type) \
void print_##type(type value) { \
printf(#type ": %d\n", value); \
}
DEFINE_PRINT(int) // 生成 void print_int(int value) { ... }
DEFINE_PRINT(long) // 生成 void print_long(long value) { ... }
5.9 预处理器调试
# 查看宏展开结果
gcc -E main.c | tail -20
# 保留注释(通常预处理会删除注释)
gcc -C -E main.c
# 保留行号标记
gcc -E main.c | grep "^#"
# 生成依赖关系(用于 Makefile)
gcc -M main.c
# main.o: main.c greet.h
# 生成不包含系统头文件的依赖
gcc -MM main.c
# main.o: main.c greet.h
# 跟踪头文件包含
gcc -H -E main.c 2>&1 | head -20
# 输出每个被包含的头文件,前面有点号表示嵌套深度
-M / -MM 依赖生成
# 在 Makefile 中自动生成依赖
DEPFLAGS = -MMD -MP
%.o: %.c
$(CC) $(CFLAGS) $(DEPFLAGS) -c -o $@ $<
-include $(OBJS:.o=.d)
要点回顾
| 要点 | 核心内容 |
|---|---|
| 预处理器 | 文本级处理器,在编译前运行 |
| 宏定义 | #define,注意括号和副作用问题 |
| 条件编译 | #ifdef / #if defined() 用于平台适配和调试开关 |
| Include Guard | #ifndef/#define/#endif 或 #pragma once |
| 预定义宏 | __FILE__, __LINE__, __GNUC__ 等 |
#pragma | 诊断控制、结构体对齐、符号禁用 |
| Token 运算符 | # 字符串化,## 连接 |
注意事项
宏不是函数: 宏是文本替换,没有类型检查,不遵循作用域规则。优先使用
static inline函数替代宏。
宏参数的括号: 宏定义中所有参数引用和整体表达式都应该用括号括起来,避免运算符优先级问题。
Include Guard 命名: 使用唯一且不易冲突的宏名,如
<项目名>_<目录>_<文件名>_H。
#pragma once的局限: 在符号链接(symlink)或硬链接的头文件场景中,#pragma once可能无法正确去重。
扩展阅读
- GCC Manual: Preprocessor — 完整预处理器文档
- C11 Standard §6.10 — 预处理指令标准
- Boost.Preprocessor — 高级预处理器技巧库
- mcpp — 可用于理解预处理器行为的参考实现
下一步
→ 06 - 优化技术:深入理解 GCC 的各级优化技术,从 -O0 到 -Ofast,LTO 和 PGO。