第3.11章: 预处理器

English

什么是预处理器

在编译器看到你的 C++ 代码之前,代码会先被 预处理器处理。预处理器对源代码执行简单的文本 变换。它不理解 C++ 的类型、作用域或语义, 只操作词法单元(token)。

预处理指令以 # 开头,在编译之前逐行处理。

#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 自增两次,几乎从来不是你想要的结果。

关于宏的警告

宏在条件编译和包含防卫中仍然是必要的。

#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
      

要点