强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 标准时为 11
__STDC_VERSION__C 标准版本号201710L (C17)
__cplusplusC++ 标准版本号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 平台
_WIN32Windows 平台

查看所有预定义宏

# 查看所有预定义宏
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 可能无法正确去重。


扩展阅读


下一步

06 - 优化技术:深入理解 GCC 的各级优化技术,从 -O0 到 -Ofast,LTO 和 PGO。