第3.11章: 预处理器
什么是预处理器
在编译器看到你的 C++ 代码之前,代码会先被 预处理器处理。预处理器对源代码执行简单的文本 变换。它不理解 C++ 的类型、作用域或语义, 只操作词法单元(token)。
预处理指令以 # 开头,在编译之前逐行处理。
- 复制文件(
#include) - 定义宏(
#define) - 条件编译(
#ifdef、#ifndef) - 嵌入外部数据(
#embed) - 编译器专属指令(
#pragma)
#include
#include 指令会把另一个文件的全部内容原样复制到
当前文件中。
#include <fast_io.h>
#include <fast_io.h> // included twice
为什么这不会破坏编译
fast_io 库在每个头文件的开头都使用了 #pragma once。
当预处理器第二次看到该头文件时,它会完全忽略其中的内容。
这可以防止重复定义,并让重复包含变得安全。
没有 #pragma once 时会发生什么
如果一个头文件没有使用 #pragma once(或
包含防卫宏),那么包含它两次就会插入两遍同样的文本:
// myheader.h (no pragma once)
void f();
#include "myheader.h"
#include "myheader.h" // inserted twice → duplicate definition error
这会破坏编译,因为该函数被定义了两次。
#include 为什么慢
因为 #include 执行的是头文件文本的完整复制粘贴,
包括它自己所有的 #include 指令,
所以大型项目会变得编译缓慢。
单个头文件可能展开成数千行代码,而这种展开会发生在 每一个包含它的翻译单元中。
#pragma once
#pragma once 确保一个头文件在每个翻译单元中只被包含
一次。
// myheader.h
#pragma once
void f();
这可以防止重复定义并加快编译速度。 所有主流编译器都支持它。
#define
#define 指令创建一个宏。
宏就是在编译之前执行的简单文本替换。
对象式宏
#define PI 3.141592653589793
#define BUFFER_SIZE 1024
当预处理器看到 PI 时,就把它替换为文本
3.141592653589793。
函数式宏
#define SQR(x) ((x) * (x))
圆括号至关重要,可以避免优先级造成的 bug。
宏的副作用
宏不会只求值一次实参,它会替换文本。 这可能造成危险的副作用:
::std::uint_least32_t i{};
println(SQR(++i));
该宏展开为:
((++i) * (++i))
这会让 i 自增两次,几乎从来不是你想要的结果。
关于宏的警告
- 没有类型检查
- 没有作用域(宏是全局的)
- 难以调试
- 当实参包含
++或--时会产生副作用 - 现代 C++ 中优先使用
constexpr与inline
宏在条件编译和包含防卫中仍然是必要的。
#embed
#embed 指令在编译期把文件内容作为数据直接嵌入
你的程序。
static constexpr auto data =
#embed "resource.bin";
与粘贴 C++ 代码的 #include 不同,#embed 用于
二进制或文本资源。
#embed 的支持情况取决于编译器与标准版本。
预处理指令汇总
#include // copy-paste another file
#pragma once // include only once
#define // define a macro
#embed // embed external data
#ifdef/#ifndef // conditional compilation (next chapter)
#endif
要点
- 预处理器在编译之前运行:它执行文本替换。
- #include 复制文件:因展开而可能很慢。
- #pragma once:防止重复包含并避免重复定义。
- #define:简单但危险;注意副作用。
- 宏会替换文本:
SQR(++i)展开为((++i)*(++i))。 - #embed:在编译期嵌入二进制/文本数据。
- 预处理器不理解 C++:它只操作词法单元。