HOWTO · C++
C++ 中的 #pragma once:包含保护与可移植性
使用 #pragma once 或包含保护防止 C++ 头文件重复包含,并根据工具链选择合适的方法。
#pragma once 是让实现对同一翻译单元中的头文件最多处理一次的预处理指令。若项目支持的所有编译器都实现它,请将它放在普通 C/C++ 头文件开头。若需要标准可移植性、要支持未知编译器,或项目已有约定,请使用名称唯一的 #ifndef/#define 包含保护。两者都不会让头文件在整个程序中只能使用一次。
在声明之前放置 #pragma once
// config.hpp
#pragma once
class Config {
public:
int port() const { return 8080; }
};
它不需要分号。预处理器会在编译前处理 #include,因此该指令会阻止在构造一个翻译单元时再次处理同一头文件文本。GCC、Clang 和 MSVC 广泛实现它,但它不是 ISO C/C++ 标准指令。请参考 GCC 的一次性头文件文档,并确认项目真正承诺支持的工具链。
验证直接和间接包含
这个已验证的三文件示例会通过 server.hpp 和 main.cpp 直接两条路径包含 config.hpp:
// server.hpp
#pragma once
#include "config.hpp"
class Server { Config config_; };
// main.cpp
#include "server.hpp"
#include "config.hpp"
int main() {
return Config{}.port() == 8080 ? 0 : 1;
}
将三个文件保存到同一目录并运行:
g++ -std=c++17 -Wall -Wextra main.cpp -o app
./app
当 config.hpp 有 #pragma once 时,没有终端输出,./app 以状态 0 结束。该示例已用 g++ (Ubuntu 15.2.0-16ubuntu1) 15.2.0 验证。只删除 config.hpp 中该行后,GCC 以状态 1 结束,并报告由直接包含和先前间接包含造成的 Config 重定义。这是按翻译单元生效的结果,未单独在 MSVC 或 Clang 上测试。
使用标准包含保护
// config.hpp
#ifndef EXAMPLE_CONFIG_HPP
#define EXAMPLE_CONFIG_HPP
class Config {
public:
int port() const { return 8080; }
};
#endif // EXAMPLE_CONFIG_HPP
首次包含会定义宏,后续包含跳过其中文本。选择由项目、目录和文件名组成的描述性唯一名称。CONFIG_H 可能与其他头文件冲突;包含 __ 或以下划线加大写字母开头的标识符保留给实现。
#include 是文本包含:在检查 C++ 声明之前,预处理器会用头文件文本替换该指令。示例中 config.hpp 先经由 server.hpp 进入,再由 main.cpp 直接进入;没有保护时,编译器会得到两个 Config 定义。因此保护应放在头文件本身,而非仅放在某个碰巧包含它的源文件。这是编译错误,不同于分别编译文件产生的链接错误。
选择机制
| 情况 | 选择 | 原因 |
|---|---|---|
所有目标编译器实现 #pragma once |
#pragma once |
简洁且没有保护宏冲突。 |
| 公共库、未知编译器或严格可移植性 | 包含保护 | 使用标准预处理指令。 |
| 已有仓库约定 | 原有约定 | 保持头文件易维护。 |
| 有意多次包含的头文件,例如 X-macro 列表 | 默认两者都不用 | 重复包含正是其目的。 |
不要默认在每个头文件中同时加入两种机制:一种通常已足够,编译器也能识别常规保护。不要宣称普遍的构建速度优势;需要时应测量。#pragma once 依赖实现识别文件身份,因此别名、生成文件、网络文件系统或特殊包含路径可能是边界条件。包含保护避开该问题,但宏必须唯一。
头文件保护不能解决的问题
保护按翻译单元工作,不能修复所有多重定义链接错误或 One Definition Rule (ODR) 问题。头文件中的非 inline 普通函数定义可能在每个 .cpp 中生成外部定义;请把普通定义放进 .cpp,或在设计需要时使用 inline 或模板。它也不能解决两个类型都需要完整定义的循环依赖:对指针或引用使用前向声明,或移动实现。C++20 模块是不同机制,import 不会文本包含头文件。
总结
能接受扩展支持时使用 #pragma once,需要标准可移植性时使用唯一的包含保护。验证直接和间接路径,并分别诊断 ODR、循环、故意重复包含和模块问题。