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.hppmain.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、循环、故意重复包含和模块问题。