预处理器

原理

编译四阶段

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)) = 16

Token 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”
__cplusplusC++ 标准版本: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) #defineconstexpr 在底层有什么区别?

建议先阅读09_函数模板 — 理解模板的编译期实例化机制,与宏的文本替换对比;01_命名空间 — 宏没有命名空间概念,理解这一点非常重要。

C 语言关联:宏在 C 语言中是定义常量和代码生成的主要手段。C++ 中优先使用 constexprinline 函数和模板替代。但条件编译和跨平台适配仍依赖预处理器。详见 C 位运算与宏