C++ 头文件保护(#ifndef 与 #pragma once)

在 C++ 项目开发中,如果同一个头文件在单个 .cpp 源文件里展开了多次,里面的结构体或类定义就会重复出现,编译时就会报出 redefinition(重复定义)错误。

为了避免这个问题,我们在编写 .h 头文件时都需要加上一层 “头文件保护”,确保同一个头文件在单个源文件中最多只展开一次。

为什么需要头文件保护?

在实际项目中,同一个头文件之所以会重复展开,通常不是因为我们粗心写了两遍相同的 #include 指令,而是因为头文件之间的 “嵌套包含”。

举个常见的例子,假设我们在 point.h 中定义了表示坐标的结构体 Point,代码如下:

// point.h
struct Point {
    int x;
    int y;
};

接着,玩家头文件 player.h 需要使用 Point 来表示玩家位置,于是在内部引入了 point.h:

// player.h
#include "point.h"

struct Player {
    Point pos;
};

到了主程序 main.cpp 中,由于同时用到了 Point 和 Player,我们在开头把这两个头文件都引入了进来:

// main.cpp
#include "point.h"
#include "player.h"

此时在预处理阶段,point.h 在 main.cpp 中的展开路径就有两条:

路径 1:main.cpp -> point.h
路径 2:main.cpp -> player.h -> point.h

可以看到,虽然我们在 main.cpp 里只写了一行 #include "point.h",但 point.h 先直接展开了一次,随后又跟着 player.h 间接展开了一次。最终 struct Point 在 main.cpp 里出现了两遍,直接导致编译报错。

当项目里有几十个头文件互相引用时,靠人工去排查谁包含了谁是不现实的。在 C++ 中,我们通常会在每个头文件内部加上头文件保护,常用的有以下 2 种方式:

  • 使用 #ifndef 宏保护:C++ 标准原生的传统方式,也叫做 “包含保护(include guard)”。
  • 使用 #pragma once 指令:现代主流编译器广泛支持的简写方式。

使用 #ifndef 宏保护

#ifndef 宏保护是 C++ 中跨平台兼容性最好的传统方式。它通过 #ifndef、#define 和 #endif 这 3 条预处理指令互相配合,给头文件打上 “是否已经展开过” 的标记。

语法:

#ifndef 宏名
#define 宏名

// 头文件的实际内容

#endif

说明:

这 3 条预处理指令各自的作用如下:

  • #ifndef 宏名:#ifndef 是 “if not defined(如果未定义)” 的缩写,用来判断后面的宏名是不是还没有定义过。
  • #define 宏名:如果上面的判断成立,说明是第 1 次遇到这个头文件。此时我们通过 #define 把该宏名定义下来,相当于贴上一张 “已展开” 的标签,再正常保留下面的头文件内容。
  • #endif:写在头文件的最末尾,用来标志 #ifndef 判断范围的结束。

关于宏定义与条件编译的更多用法,后面两节会详细介绍,这里先把这 3 行当成固定搭配即可。下面我们给 point.h 加上 #ifndef 头文件保护,代码如下:

#ifndef POINT_H
#define POINT_H

struct Point {
    int x;
    int y;
};

#endif

平时写项目时,我们只需要像上面这样在 point.h 里写一遍保护代码就可以了。为了方便小伙伴们直接在线运行观察效果,下面我们把预处理器在 main.cpp 中连续展开两次 point.h 后的样子模拟出来,请看示例 1。

示例 1:使用 #ifndef 防止重复展开

#include <iostream>

// 模拟第 1 次展开 point.h
#ifndef POINT_H
#define POINT_H
struct Point {
    int x;
    int y;
};
#endif

// 模拟第 2 次展开 point.h
#ifndef POINT_H
#define POINT_H
struct Point {
    int x;
    int y;
};
#endif

int main() {
    Point p = {3, 5};
    std::cout << p.x + p.y << '\n';
    return 0;
}

运行结果如下。

8

分析:

从运行结果可以看出,带有 #ifndef 保护的代码块虽然连续出现了两次,但程序依然顺利通过编译并输出了 8。预处理器在背后的处理过程分为以下 2 步:

  • 第 1 次遇到 #ifndef POINT_H:此时 POINT_H 这个宏还不存在,条件成立。预处理器接着执行 #define POINT_H 把宏定义好,并将中间的 struct Point 结构体保留下来。
  • 第 2 次遇到 #ifndef POINT_H:由于第 1 次已经定义过 POINT_H 了,此时条件不成立。预处理器会直接跳过中间的所有代码,一直跳到对应的 #endif,这样就避免了 struct Point 重复定义。

注意 在使用 #ifndef 保护头文件时,每个头文件的宏名在整个项目中都必须是唯一的。我们通常按照 “文件名全大写_H” 的格式来命名,比如 POINT_H、MATH_UTILS_H。千万不要让两个不同的头文件使用相同的宏名,否则第 1 个头文件展开后,预处理器就会误以为第 2 个头文件也展开过了,直接把它的内容跳过。

使用 #pragma once 指令(推荐)

使用 #ifndef 虽然兼容性好,但每次都要手写首尾 3 行指令,还得留心防止宏名重名或拼写不一致。在现代 C++ 开发中,GCC、Clang、MSVC 等主流编译器都支持更简洁的头文件保护指令:#pragma once。

语法:

#pragma once

// 头文件的实际内容

说明:

#pragma once 只需要写在头文件的第一行,注意行尾不要加英文分号(;)。它的作用是直接告诉编译器:在编译当前 .cpp 源文件时,这个头文件最多只包含一次。

下面我们使用 #pragma once 来改写 point.h,代码如下:

#pragma once

struct Point {
    int x;
    int y;
};

接着我们在同一个文件夹下新建 main.cpp,故意连续引入两次 point.h,模拟它在复杂项目中多次嵌套包含的场景。

示例 2:#pragma once 保护头文件

#include <iostream>
#include "point.h"
#include "point.h"

int main() {
    Point p = {3, 5};
    std::cout << p.x + p.y << '\n';
    return 0;
}

在本地终端进入该文件夹,执行 g++ main.cpp -o app 编译并运行后,运行结果如下。

8

分析:

与前面的 #ifndef 方式相比,#pragma once 省去了末尾的 #endif,也不需要我们手动去起宏名,从根源上避免了宏名冲突或手误拼错宏名的问题。

在实际开发中,我们可以按以下 2 种情况来选择:

  • 新建现代 C++ 项目:优先使用 #pragma once,只需在头文件顶部写一行,既干净利落又不容易出错。
  • 维护已有项目或公共库:如果项目原本统一使用的是 #ifndef,或者需要兼容极少数不支持 #pragma once 的专用编译器,就继续沿用 #ifndef,保持团队风格一致。

常见问题

1. 加了头文件保护,就能把普通函数的函数体直接写进 .h 头文件里吗?

不能。小伙伴们一定要分清一个重要边界:头文件保护只能防止 “同一个头文件在单个 .cpp 源文件内重复展开”,但防不住 “多个不同的 .cpp 源文件各自包含一次该头文件”。

在多文件编程中,假如我们在 math_utils.h 里写了 add() 的函数体,而 main.cpp 和 other.cpp 都包含了它。由于这两个 .cpp 文件是各自独立编译的,它们都会把 add() 的函数体编译进各自的 .o 目标文件中。

等到最后链接器把所有 .o 文件组装在一起时,就会发现两份相同的 add() 实现撞车了,依然会报出 multiple definition(重复定义)错误。因此,即使加了头文件保护,普通函数的函数体仍然要放在 .cpp 源文件中。

2. 使用 #ifndef 时,为什么不建议把宏名写成下划线开头?

有些早期代码喜欢把头文件保护宏写成 __POINT_H__ 或 _POINT_H。但在 C++ 标准规范中,凡是以双下划线(__)开头,或者以 “单下划线 + 大写字母” 开头的名称,都是专门留给编译器和标准库内部使用的保留名称。

为了避免和系统底层宏发生命名冲突,平时我们定义头文件保护宏时,直接写 POINT_H 或加上项目前缀写成 DEMO_POINT_H 即可,不要在开头加下划线。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号