一、预处理器的本质
1.1 什么是预处理器
预处理器是编译器前端的一个组件,负责在正式编译之前处理源代码。
核心特点:
- 不是编译器的一部分,但与编译器紧密配合
- 基于纯文本操作,不理解C++语法
- 处理的是源文件文本,不是AST或中间代码
1.2 在编译流程中的位置
源代码 (.cpp/.h) ↓ ┌─────────────────────┐ │ 预处理器 │ ← 第一阶段 │ (Preprocessor) │ └─────────────────────┘ ↓ 纯文本输出 ┌─────────────────────┐ │ 编译器 │ ← 第二阶段 │ (Compiler) │ └─────────────────────┘ ↓ 目标文件 (.o) ┌─────────────────────┐ │ 链接器 │ ← 第三阶段 │ (Linker) │ └─────────────────────┘ ↓ 可执行文件二、预处理器的标准定义
根据ISO C++ 标准,预处理器必须实现以下功能 / 预处理指令:
| 功能 | 说明 |
|---|---|
| 文件包含 | #include |
| 宏定义 | #define,#undef |
| 条件编译 | #if,#elif,#else,#endif,#ifdef,#ifndef |
| 行控制 | #line |
| 错误指令 | #error |
| 杂项指令 | #pragma,[#pragma region/#pragma endregion;#pragma once] |
| 空指令 | # |
三、预处理器的详细工作模式
3.1 逐行扫描机制
预处理器按物理行顺序扫描源代码:
源文件: Line 1: #define PI 3.14 Line 2: double area = PI * r * r; Line 3: #ifdef DEBUG Line 4: printf("Debug\n"); Line 5: #endif扫描过程:
预处理器 → 读取Line 1 → 发现#define → 记录宏 预处理器 → 读取Line 2 → 发现PI → 替换为3.14 预处理器 → 读取Line 3 → 发现#ifdef → 检查条件 预处理器 → 读取Line 4 → 根据条件决定保留/丢弃 预处理器 → 读取Line 5 → 发现#endif → 条件块结束3.2 宏展开的完整流程
#define SQUARE(x) ((x) * (x)) #define MAX(a, b) ((a) > (b) ? (a) : (b)) int main() { int result = MAX(2, SQUARE(3)); return 0; }展开步骤:
步骤1: 发现 MAX(2, SQUARE(3)) 替换为: ((2) > (SQUARE(3)) ? (2) : (SQUARE(3))) 步骤2: 继续扫描展开后的代码 发现 SQUARE(3),展开为: ((3) * (3)) 步骤3: 最终结果: ((2) > (((3) * (3))) ? (2) : (((3) * (3)))) 步骤4: 输出给编译器3.3 预处理器状态机
┌──────────────┐ │ 开始扫描 │ └──────┬───────┘ ↓ ┌──────────────┐ │ 遇到#开头? │──否→ 复制到输出 └──────┬───────┘ ↓ 是 ┌──────────────┐ │ 识别预处理指令│ └──────┬───────┘ ↓ ┌──────────┐────────┴┐────────────┐───────┐───────┐ ↓ ↓ ↓ ↓ #include #define #if/#elif... #error #line ... ↓ ↓ ↓ ↓ ↓ 插入 记录宏 条件 错误 行号 文件 定义 编译 指令 控制 ↓ ↓ ↓ ↓ ↓ └──────────┘─────────┘──┬─────────┘───────┘───────┘ ↓ 继续扫描四、预处理器的具体任务
4.1 任务一:文件包含处理
工作原理:
#include <iostream> #include "myheader.h"预处理器将指定文件内容完全插入到#include位置:
┌────────────────────────────────────┐ │ 源文件 main.cpp │ │ #include "myheader.h" │ │ int main() { foo(); return 0; } │ └────────────────────────────────────┘ ↓ 预处理 ┌────────────────────────────────────┐ │ // myheader.h 的内容被插入 │ │ void foo(); │ │ int main() { foo(); return 0; } │ └────────────────────────────────────┘两种包含方式的区别:
#include <stdio.h> // 尖括号:系统目录查找 #include "myfile.h" // 引号:当前目录优先,再系统目录4.2 任务二:宏指令
4.2.1宏定义
宏替换的本质:
预处理器工作 = 文本替换游戏 = 找"宏名" → 替换为"定义文本"基本语法:
#define 标识符 替换文本 #define 标识符(参数) 替换文本 #define 标识符(参数...) 替换文本对象宏(常量宏):
#define PI 3.14159 #define MAX_SIZE 1024 #define VERSION "1.0.0"函数宏:
#define SQUARE(x) ((x) * (x)) #define MAX(a, b) ((a) > (b) ? (a) : (b)) #define IS_EVEN(x) (((x) % 2) == 0)多行宏:
#define SWAP(a, b) \ do { \ auto _temp = (a); \ (a) = (b); \ (b) = _temp; \ } while(0)可变参数宏:
#define LOG(fmt, ...) fprintf(stderr, fmt, ##__VA_ARGS__) #define DEBUG(fmt, args...) printf("[DEBUG] " fmt "\n", ##args)展开时机:
#define VALUE 100 int a = VALUE; // VALUE → 100 #define VALUE 200 // 重新定义宏 int b = VALUE; // VALUE → 2004.2.2取消宏定义
语法:
#undef 标识符作用:取消已定义的宏,释放命名空间
示例:
#define PI 3.14 double a = PI; // OK: 3.14 #undef PI double b = PI; // ❌ PI未定义,编译错误使用场景:
#define BUFFER_SIZE 1024 // 使用BUFFER_SIZE char buffer[BUFFER_SIZE]; // 重新定义前先取消 #undef BUFFER_SIZE #define BUFFER_SIZE 20484.3 任务三:条件编译
条件判断发生在预处理阶段:
#define DEBUG_MODE 1 #if DEBUG_MODE == 1 void log() { cout << "debug"; } #else void log() { } // 实际编译这个 #endif编译器视角(预处理后):
// 预处理后的代码(DEBUG_MODE == 1时): void log() { cout << "debug"; } // 预处理后的代码(DEBUG_MODE == 0时): void log() { }4.3.1#if- 条件编译
语法:
#if 条件表达式 代码块1 #endif条件表达式:
#define ENABLE_FEATURE 1 #if ENABLE_FEATURE == 1 void featureOn() { cout << "Feature enabled"; } #endif #if __cplusplus >= 201703L void modernFeature() { /* C++17特性 */ } #endif算数表达式:
#define BITS 32 #if BITS == 32 using int_t = int; #elif BITS == 64 using int_t = long; #else using int_t = short; #endif4.3.2#elif- 否则如果
语法:
#if 条件1 代码块1 #elif 条件2 代码块2 #elif 条件3 代码块3 #else 代码块4 #endif示例:
#define PLATFORM 2 // 1=Windows, 2=Linux, 3=macOS #if PLATFORM == 1 const char* PATH_SEP = "\\"; const char* OS_NAME = "Windows"; #include <windows.h> #elif PLATFORM == 2 const char* PATH_SEP = "/"; const char* OS_NAME = "Linux"; #include <unistd.h> #elif PLATFORM == 3 const char* PATH_SEP = "/"; const char* OS_NAME = "macOS"; #include <mach-o/dyld.h> #else #error "Unsupported platform" #endif4.3.3#else- 否则
语法:
#if 条件 代码块1 #else 代码块2 #endif示例:
#ifdef DEBUG #define LOG(x) cout << "[DEBUG] " << x << endl #else #define LOG(x) // 空操作 #endif4.3.4#endif- 结束条件编译
作用:结束#if/#ifdef/#ifndef块
4.3.5#ifdef- 如果定义了宏
语法:
#ifdef 宏名 代码块 #endif等价于:
#if defined(宏名) 代码块 #endif示例:
#define ENABLE_LOGGING #ifdef ENABLE_LOGGING void log(const char* msg) { cout << msg << endl; } #else void log(const char* msg) { } // 空实现 #endif4.3.6#ifndef- 如果未定义宏
语法:
#ifndef 宏名 代码块 #endif最典型应用 - Include Guard:
// file.h #ifndef _FILE_H_ #define _FILE_H_ // 头文件内容放在这里 // 确保即使多次包含,内容也只出现一次 class MyClass { // ... }; #endif // _FILE_H_4.4 任务四:行号控制
保持错误报告准确性:
// mytemplate.h (第5行开始) void process(int x, int y); // main.cpp #include "mytemplate.h" // 假设mytemplate.h的第5行是void process...使用#line控制行号:
#line 100 "generated.cpp" auto code = "x + y"; // 编译器认为这是generated.cpp的第100行4.5 任务五:错误指令
语法:
#error 错误消息触发条件:
#if __cplusplus < 201703L #error "This code requires C++17 or later" #endif完整示例:
// 检查平台 #if !defined(WIN32) && !defined(LINUX) && !defined(MACOS) #error "Unsupported platform. Must define WIN32, LINUX, or MACOS" #endif // 检查编译器 #ifdef __GNUC__ #if __GNUC__ < 8 #error "GCC 8.0 or later is required" #endif #endif // 检查位数 #if INTPTR_MAX != INT64_MAX #error "This code requires 64-bit environment" #endif与条件编译结合:
#define PROTOCOL_VERSION 2 #if PROTOCOL_VERSION == 1 #define HANDLE_PACKET void handlePacketV1() #elif PROTOCOL_VERSION == 2 #define HANDLE_PACKET void handlePacketV2() #else #error "PROTOCOL_VERSION must be 1 or 2" #endif编译时主动报错:
#define MIN_STD 201703L #if __cplusplus < MIN_STD #error "This code requires C++17 or later" #endif使用场景:
- 强制编译器版本要求
- 检测不支持的平台
- 验证必需的配置
4.6 任务六:杂项指令 #pragma
编译器特定行为:
#pragma once // 只包含一次(替代Include Guard) #pragma pack(1) // 设置结构体对齐为1字节 struct Packed { char a; int b; }; #pragma pack() // 恢复默认对齐 #pragma warning(disable: 1234) // MSVC: 禁用某个警告 #pragma GCC diagnostic push // GCC: 警告控制 #pragma GCC diagnostic ignored "-Wformat"4.6.1 #pragma once- 仅包含一次
作用:确保头文件只被包含一次(现代Include Guard替代方案)
语法:
// file.h #pragma once class MyClass { // ... };与Include Guard对比:
// Include Guard(传统) #ifndef _MY_CLASS_H_ #define _MY_CLASS_H_ // ... #endif // Pragma Once(现代) #pragma once // ...优点:
- 更简洁
- 避免命名冲突
- 某些编译器优化处理速度
缺点:
- 非C++标准(但主流编译器都支持)
- 无法防止物理位置不同但内容相同的文件
- 无法用于条件包含
4.6.2#pragma pack- 结构体对齐
作用:控制结构体成员内存对齐方式
语法:
#pragma pack(show) // 显示当前对齐值 #pragma pack(push) // 保存当前对齐值 #pragma pack(pop) // 恢复之前保存的对齐值 #pragma pack(n) // 设置对齐为n字节 #pragma pack(push, n) // 保存并设置对齐示例:
// 默认对齐(通常是8字节) struct Default { char a; // 偏移0 double b; // 偏移8 char c; // 偏移16 // 结构体大小: 24字节 }; // 紧凑对齐(1字节) #pragma pack(push, 1) struct Packed { char a; // 偏移0 double b; // 偏移1 char c; // 偏移9 // 结构体大小: 10字节 }; #pragma pack(pop) // 对齐值可以是 1, 2, 4, 8, 16 等 #pragma pack(push, 2) struct TwoByteAligned { int x; // 偏移0 char y; // 偏移4 // 结构体大小: 6字节 }; #pragma pack(pop)使用场景:
- 网络协议数据包(固定格式)
- 文件格式解析
- 跨语言数据结构
4.6.3#pragma warning- 警告控制(MSVC)
语法:
#pragma warning(disable: nnnn) // 禁用某个警告 #pragma warning(push) // 保存当前警告状态 #pragma warning(pop) // 恢复之前保存的状态 #pragma warning(once: nnnn) // 某个警告只显示一次 #pragma warning(default: nnnn) // 将警告恢复为默认状态示例:
#pragma warning(disable: 4996) // 禁用某些函数的不安全警告 // 危险的代码,但编译不报警告 strcpy_s(dest, src); #pragma warning(default: 4996) // 恢复警告状态批量控制:
#pragma warning(push) #pragma warning(disable: 4100) // 未使用的参数 #pragma warning(disable: 4189) // 初始化后未使用的变量 void func(int unused_param) { int unused_var = 0; } #pragma warning(pop) // 恢复警告状态4.6.4#pragma GCC- GCC特定指令
语法:
#pragma GCC diagnostic kind option示例:
#pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat" // 忽略格式字符串警告 #pragma GCC diagnostic ignored "-Wunused" // 忽略未使用警告 void func() { // 可能有警告的代码 } #pragma GCC diagnostic pop // 恢复警告状态常见GCC警告选项:
#pragma GCC diagnostic ignored "-Wdeprecated" // 弃用警告 #pragma GCC diagnostic ignored "-Wuninitialized" // 未初始化警告 #pragma GCC diagnostic ignored "-Wall" // 所有警告4.6.5#pragma message- 编译时消息
语法:
#pragma message("消息文本")示例:
#define VERSION 2 #if VERSION == 1 #pragma message("Compiling with VERSION 1") #define API_PREFIX "v1" #elif VERSION == 2 #pragma message("Compiling with VERSION 2") #define API_PREFIX "v2" #endif // 输出: Compiling with VERSION 2与#error结合:
#define BUILD_MODE "release" #if BUILD_MODE == "debug" #pragma message("Building in DEBUG mode") #elif BUILD_MODE == "release" #pragma message("Building in RELEASE mode") #else #error "Invalid BUILD_MODE" #endif4.6.6#pragma region- 代码折叠标记(IDE)
语法:
#pragma region 区域名称 // 代码 #pragma endregion示例:
class MyClass { #pragma region Constructor public: MyClass() : value_(0) {} ~MyClass() {} #pragma endregion #pragma region Private Methods private: void init() {} void cleanup() {} #pragma endregion #pragma region Data private: int value_; std::string name_; #pragma endregion };作用:仅供IDE识别,编译时被忽略,用于代码组织和折叠。
4.6.7 预定义宏
1.标准预定义宏
| 宏名 | 说明 | 示例 |
|---|---|---|
__FILE__ | 当前源文件名 | "main.cpp" |
__LINE__ | 当前行号 | 42 |
__DATE__ | 编译日期 | "Jul 6 2026" |
__TIME__ | 编译时间 | "12:34:56" |
__STDC__ | 是否符合C标准 | 1 |
__STDC_VERSION__ | C标准版本 | 201710L(C17) |
__cplusplus | C++标准版本 | 201703L(C++17) |
__func__ | 当前函数名 | "main" |
2.编译器特定宏
GCC/Clang:
__GNUC__ // GCC主版本号 __GNUC_MINOR__ // GCC次版本号 __clang__ // 是否Clang编译器 __clang_major__ // Clang主版本号MSVC:
_MSC_VER // MSVC编译器版本 _MSC_FULL_VER // MSVC完整版本 _WIN32 // Windows 32/64位 _WIN64 // Windows 64位 _M_IX86 // x86架构 _M_X64 // x64架构示例:
#ifdef __GNUC__ #define COMPILER "GCC " __VERSION__ #elif defined(_MSC_VER) #define COMPILER "MSVC " _MSC_VER #elif defined(__clang__) #define COMPILER "Clang " __clang_version__ #else #define COMPILER "Unknown" #endif // C++版本检测 #if __cplusplus == 199711L #define STD_VERSION "C++98" #elif __cplusplus == 201103L #define STD_VERSION "C++11" #elif __cplusplus == 201402L #define STD_VERSION "C++14" #elif __cplusplus == 201703L #define STD_VERSION "C++17" #elif __cplusplus == 202002L #define STD_VERSION "C++20" #elif __cplusplus >= 202302L #define STD_VERSION "C++23+" #endif4.6.8 空指令 - #
语法:
#作用:预定义宏必须包含一个#指令,但实际上是空操作
实际应用:通常不直接使用
五、预处理器存在的意义
5.1 历史原因:兼容性设计
C语言诞生于1972年(Unix系统):
- 当时的硬件资源极度有限
- 编译器设计必须简单高效
- 文本替换是最简单的"代码生成"方式
5.2 核心价值
| 价值 | 说明 |
|---|---|
| 代码复用 | #include实现头文件复用 |
| 条件编译 | 同一源码适配不同平台 |
| 常量定义 | 避免"魔法数字" |
| 性能优化 | 宏展开无函数调用开销 |
| 元编程 | 编译时计算和代码生成 |
5.3 为什么不用其他机制替代
问题:C++有没有替代预处理器的方案?
现有替代方案的局限性:
// constexpr常量(现代C++) constexpr int PI = 3.14; // 好,但不能完全替代宏 // constexpr函数(现代C++) constexpr int square(int x) { return x * x; } // 好,但有局限 // const变量(现代C++) const int MAX_SIZE = 100; // 好,但不能用于数组维度 // 模板(现代C++) template<int N> struct Array {}; // 模板参数可以用常量 Array<MAX_SIZE> arr; // 但MAX_SIZE必须是编译期常量预处理器的不可替代性情况:
- 平台差异处理(预处理器最擅长)
- 调试开关(预处理器天生适合)
- Include Guard(必须用预处理)
// 1. 平台差异处理(预处理器最擅长) #ifdef _WIN32 #include <windows.h> #elif __linux__ #include <unistd.h> #endif // 2. 调试开关(预处理器天生适合) #ifdef DEBUG #define LOG(x) cout << x << endl #else #define LOG(x) // 完全消除,无任何运行时开销 #endif // 3. Include Guard(必须用预处理) #ifndef MY_HEADER_H #define MY_HEADER_H // ... #endif六、预处理器的典型应用场景
6.1 Include Guard / Pragma Once
防止重复包含:
// 传统方式 #ifndef _MY_HEADER_H_ #define _MY_HEADER_H_ // 头文件内容 #endif // 现代方式 #pragma once // 简洁,但非标准6.2 跨平台开发框架
// platform.h #ifndef PLATFORM_H_ #define PLATFORM_H_ // 检测操作系统 #if defined(_WIN32) || defined(_WIN64) #define PLATFORM_WINDOWS #define PATH_SEPARATOR '\\' #define SOCKET_CLOSE closesocket #include <winsock2.h> #elif defined(__linux__) #define PLATFORM_LINUX #define PATH_SEPARATOR '/' #define SOCKET_CLOSE close #include <sys/socket.h> #include <unistd.h> #elif defined(__APPLE__) #define PLATFORM_MACOS #define PATH_SEPARATOR '/' #define SOCKET_CLOSE close #include <sys/socket.h> #include <unistd.h> #else #error "Unsupported platform" #endif // 检测编译器 #if defined(__GNUC__) #define COMPILER_GCC (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) #define ALIGN(n) __attribute__((aligned(n))) #elif defined(_MSC_VER) #define COMPILER_MSVC _MSC_VER #define ALIGN(n) __declspec(align(n)) #else #warning "Unknown compiler" #endif // C++版本检测 #if __cplusplus >= 201703L #define HAS_CPP17 1 #if __cplusplus >= 202002L #define HAS_CPP20 1 #endif #endif // 调试模式 #ifdef DEBUG #define LOG(fmt, ...) fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__) #define ASSERT(expr) if(!(expr)) abort() #else #define LOG(fmt, ...) // 空操作 #define ASSERT(expr) // 空操作 #endif // API导出宏 #ifdef PLATFORM_WINDOWS #ifdef BUILDING_DLL #define API __declspec(dllexport) #else #define API __declspec(dllimport) #endif #else #define API __attribute__((visibility("default"))) #endif #endif // PLATFORM_H_6.3 日志系统
// logger.h #ifndef LOGGER_H_ #define LOGGER_H_ #include <iostream> #include <sstream> enum LogLevel { LOG_DEBUG, LOG_INFO, LOG_WARNING, LOG_ERROR, LOG_FATAL }; #ifndef LOG_LEVEL #define LOG_LEVEL LOG_INFO #endif #define LOG(level, msg) \ do { \ if (level >= LOG_LEVEL) { \ std::cerr << "[" << #level << "] " << msg << std::endl; \ } \ } while(0) #define LOG_DEBUG(msg) LOG(LOG_DEBUG, msg) #define LOG_INFO(msg) LOG(LOG_INFO, msg) #define LOG_WARN(msg) LOG(LOG_WARNING, msg) #define LOG_ERROR(msg) LOG(LOG_ERROR, msg) #define LOG_FATAL(msg) LOG(LOG_FATAL, msg) #define ASSERT(expr, msg) \ do { \ if (!(expr)) { \ LOG_ERROR(msg); \ std::abort(); \ } \ } while(0) #endif // LOGGER_H_6.4 导出符号控制
// dllexport.h #ifdef BUILDING_DLL #define DLL_API __declspec(dllexport) #else #define DLL_API __declspec(dllimport) #endif // 使用 class DLL_API MyClass { // ... };6.5 废弃警告
#define DEPRECATED __declspec(deprecated) #define OBSOLETE(msg) __declspec(deprecated(msg)) OBSOLETE("Use NewFunc instead") void OldFunc() {} // 编译器会警告:warning: 'OldFunc' is deprecated: Use NewFunc instead6.6 弱符号与链接控制
// 强制内联 #define FORCE_INLINE __attribute__((always_inline)) // 防止链接 #define WEAK_SYMBOL __attribute__((weak)) // 强制符号导出 #define EXPORTED __attribute__((visibility("default")))6.7 编译时断言
// C++11 static_assert static_assert(sizeof(int) >= 4, "int must be at least 32 bits"); // 预处理阶段断言(C++98兼容) #define STATIC_ASSERT(expr) typedef char static_assert_[(expr) ? 1 : -1] STATIC_ASSERT(sizeof(void*) >= 4); // 编译失败时显示数组大小为负6.8 代码生成工具
// 自动生成getter/setter #define PROPERTY(type, name) \ type name##_; \ type get##name() const { return name##_; } \ void set##name(type val) { name##_ = val; } class Widget { PROPERTY(int, width) PROPERTY(int, height) PROPERTY(string, title) }; // 展开后生成6个函数6.9 特性开关
// features.h #ifndef FEATURES_H_ #define FEATURES_H_ // 特性开关 #define FEATURE_NETWORKING 1 #define FEATURE_DATABASE 1 #define FEATURE_EXPERIMENTAL 0 #define FEATURE_LEGACY 1 // 根据开关条件编译 #if FEATURE_NETWORKING #include "network.h" #define NetworkManager CachedNetworkManager #else #define NetworkManager void #endif #if FEATURE_DATABASE #include "database.h" using DBConnection = PostgreConnection; #else using DBConnection = void; #endif #if FEATURE_EXPERIMENTAL #pragma message("Compiling with experimental features enabled") #define EXPERIMENTAL_API __attribute__((deprecated)) #else #define EXPERIMENTAL_API #endif // 特性检测宏 #define HAS_FEATURE(name) (FEATURE_##name) #if HAS_FEATURE(EXPERIMENTAL) void experimentalFunc() { // 实验性功能 } #endif #endif // FEATURES_H_七、预处理器的局限性
7.1 无类型检查
#define SQUARE(x) ((x) * (x)) int a = SQUARE("hello"); // ❌ 预处理不报错,编译可能报错7.2 调试困难
#define MAX(a, b) ((a) > (b) ? (a) : (b)) int x = MAX(i++, j++); // 调试时看到的不是宏,是展开后的代码7.3 命名空间污染
#define max(a, b) ((a) > (b) ? (a) : (b)) std::max(1, 2); // ❌ 宏替换干扰std::max现代C++的解决方案:
#include <algorithm> using std::max; // 引入到命名空间 constexpr auto MAX = [](auto a, auto b) { return a > b ? a : b; };八、查看预处理结果
GCC/Clang
g++ -E source.cpp > preprocessed.cpp # 只预处理,输出到文件 g++ -E source.cpp | head -100 # 只预处理,查看前100行MSVC
cl /P source.cpp # 生成 source.i 预处理文件示例
// source.cpp #define VALUE 42 int x = VALUE; // 预处理后 preprocessed.cpp # 1 "source.cpp" int x = 42;九、总结
| 方面 | 内容 |
|---|---|
| 本质 | 纯文本替换器,在编译前处理源代码 |
| 位置 | 编译流程的第一步 |
| 任务 | 文件包含、宏展开、条件编译、错误指令、杂项指令 |
| 优势 | 代码复用、跨平台、条件编译、元编程 |
| 局限 | 无类型检查、调试困难、命名污染 |
| 现代替代 | constexpr、inline、template、const |
| 不可替代 | Include Guard、跨平台预处理、编译开关 |
预处理器是C/C++的灵魂特性之一,它让同一套源码可以灵活应对不同编译环境,即使在现代C++中功能强大的模板和constexpr出现后,预处理器依然在跨平台开发和条件编译领域发挥着不可替代的作用。