预处理器
原理
编译四阶段
C++ 的编译流程分为四个阶段:预处理 → 编译 → 汇编 → 链接。预处理器位于第一阶段,其本质是一个纯文本替换引擎——它不理解 C++ 语法,只是机械地进行宏展开、文件包含和条件编译。
预处理阶段的输出是”翻译单元”(translation unit)——所有 #include 的文件被内联、所有宏被展开、条件编译块被判定、注释被移除。可通过 g++ -E source.cpp -o source.i 观察。
文本替换的本质
预处理器维护一个宏定义表(内部字典),遇到标识符时查找此表。若匹配则替换,替换后的文本若再含宏标识符则递归展开。预处理器不理解如下 C++ 概念:类型、作用域、重载、模板、constexpr。
这也是为什么宏参数和宏体都要用括号包裹——预处理器不做运算,生成的代码直接交给编译器按运算符优先级解析:
#define BAD_SQUARE(x) x * x
// BAD_SQUARE(3+1) → 3+1*3+1 = 7 (期望 16)
#define SQUARE(x) ((x) * (x))
// SQUARE(3+1) → ((3+1)*(3+1)) = 16Token Pasting(##)
## 将两个预处理记号连接成新记号。它在宏展开流程中的求值优先级高于宏展开——先进行 token 连接,再进行展开。因此需要双层宏来实现”先展开参数再连接”:
#define CONCAT(a, b) a ## b
#define EXPAND_AND_CONCAT(a, b) CONCAT(a, b)
// CONCAT(counter, __LINE__) → counter__LINE__ (字面连接)
// EXPAND_AND_CONCAT(counter, __LINE__) → counter42 (先展开再连接)Stringification(#)
# 将宏参数转为字符串字面量,同时对特殊字符(\、")进行转义:
#define STR(x) #x
#define DBG(var) std::cout << #var " = " << (var) << std::endl
// DBG(x + y) → std::cout << "x + y" " = " << (x + y) << std::endl条件编译与链接器去重
#ifdef/#ifndef/#endif 在预处理阶段决定哪些代码会被保留。这完全发生在编译之前,被排除的代码不会进入翻译单元。
模板函数的实例化会在多个编译单元中产生相同代码。链接器通过 COMDAT 折叠(或 ICF)进行去重,最终只保留一份。这与条件编译形成了互补——前者控制”哪些实例化需要生成”,后者控制”生成代码的编译范围”。
预处理器与 C 语言的关系
预处理器的语法源自 C 语言。C 中的 #define 用法和陷阱见 05_位运算与硬件操作 的宏部分。
语法
include
#include <iostream> // 系统头文件路径
#include "myheader.h" // 先在当前目录搜索,再搜索系统路径define / undef
#define PI 3.14159
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SWAP(a, b) do { auto tmp = (a); (a) = (b); (b) = tmp; } while(0)
#undef MAX // 取消宏定义使用
do { ... } while(0)包裹多语句宏体可防止悬挂 else 问题。
可变参数宏(C++11)
#define LOG(fmt, ...) printf(fmt, ##__VA_ARGS__)
// ##__VA_ARGS__ 是 GCC/Clang 扩展:空参数时自动移除前面的逗号
// C++20 标准替代:__VA_OPT__条件编译
#ifdef DEBUG
#define DLOG(x) std::cout << x << std::endl
#else
#define DLOG(x) ((void)0)
#endif
#if __cplusplus >= 201703L
// C++17 及以上
#endif
#if defined(__GNUC__) && !defined(__clang__)
// GCC 编译器
#elif defined(__clang__)
// Clang 编译器
#endif头文件守卫
#ifndef MYHEADER_H
#define MYHEADER_H
// 头文件内容
#endif
// 或使用(非标准但广泛支持)
#pragma once预定义宏
| 宏 | 含义 |
|---|---|
__FILE__ | 当前源文件名(字符串) |
__LINE__ | 当前行号(整数) |
__DATE__ | 编译日期 “Mmm dd yyyy” |
__TIME__ | 编译时间 “hh:mm:ss” |
__cplusplus | C++ 标准版本:199711L(C++98), 201103L(C++11), 201703L(C++17), 202002L(C++20) |
__func__ | 当前函数名(C++11,非宏) |
其他指令
#error "Unsupported platform" // 编译期报错
#pragma pack(1) // 结构体字节对齐
#pragma message("compiling...") // 编译期输出信息实践
力扣题目:无专属练习题。预处理器是构建系统和跨平台代码的核心——实际练习建议在真实项目中配置 CMake 的 Debug/Release 构建,观察 #ifdef NDEBUG 如何控制断言行为。
AI 自检:1) 写出一个宏 SWAP 使 if (a > b) SWAP(a, b); else a = 0; 能正确编译;2) 如何用预处理器实现”调试时打印日志、发布时不产生任何代码”?3) #define 和 constexpr 在底层有什么区别?
建议先阅读:09_函数模板 — 理解模板的编译期实例化机制,与宏的文本替换对比;01_命名空间 — 宏没有命名空间概念,理解这一点非常重要。
C 语言关联:宏在 C 语言中是定义常量和代码生成的主要手段。C++ 中优先使用 constexpr、inline 函数和模板替代。但条件编译和跨平台适配仍依赖预处理器。详见 C 位运算与宏。