C++头文件保护

C++头文件保护(Header Guard),也被称为预处理器宏,是一种用于防止同一个头文件被多次包含的机制,以避免重复定义和编译错误。头文件保护使用预处理器的条件编译指令来实现。

C++头文件保护(Header Guard),也被称为预处理器宏,是一种用于防止同一个头文件被多次包含的机制,以避免重复定义和编译错误。头文件保护使用预处理器的条件编译指令来实现。

重复定义问题

前向声明和定义的课程中,我们注意到变量或函数标识符只能有一个定义(一个定义规则)。因此,多次定义变量标识符的程序将导致编译错误:

int main()
{
    int x; // 这是变量 x 的定义
    int x; // 编译错误:重复定义

    return 0;
}

样,多次定义函数的程序也会导致编译错误:

#include <iostream>

int foo() // 这是函数 foo 的定义
{
    return 5;
}

int foo() // 编译错误:重复定义
{
    return 5;
}

int main()
{
    std::cout << foo();
    return 0;
}

虽然这些程序很容易修复(删除重复的定义),但使用头文件时,很容易出现头文件中的定义被多次包含的情况。当一个头文件 #include 另一个头文件时(这很常见),就会发生这种情况。

看以下示例:

square.h:

int getSquareSides()
{
    return 4;
}

wave.h:

#include "square.h"

main.cpp:

#include "square.h"
#include "wave.h"

int main()
{
    return 0;
}

这个程序无法编译! 首先,main.cpp #includes square.h,它将函数 getSquareSides 的定义复制到 main.cpp 中。 然后 main.cpp #includes wave.h,其中 #include square.h 本身。 这会将 square.h 的内容(包括函数 getSquareSides 的定义)复制到 wave.h 中,然后将其复制到 main.cpp 中。

因此,在解决所有#includes之后,main.cpp最终看起来像这样:

int getSquareSides()  // 来自 square.h
{
    return 4;
}

int getSquareSides() // 来自wave.h(通过square.h)
{
    return 4;
}

int main()
{
    return 0;
}

重复的定义和编译错误。 每个文件单独来说都很好。 然而,由于 main.cpp 最终 #include square.h 的内容两次,我们遇到了问题。 如果wave.h需要getSquareSides(),而main.cpp同时需要wave.h和square.h,你会如何解决这个问题?

头文件保护

好消息是,我们可以通过一种称为头文件保护(也称为包含保护)的机制来避免上述问题。 头文件保护是条件编译指令,采用以下形式:

#ifndef SOME_UNIQUE_NAME_HERE
#define SOME_UNIQUE_NAME_HERE

// your declarations (and certain types of definitions) here

#endif

当此标头为 #included 时,预处理器会检查先前是否已定义 SOME_UNIQUE_NAME_HERE。 如果这是我们第一次包含标头,则 SOME_UNIQUE_NAME_HERE 将不会被定义。 因此,它 #defines SOME_UNIQUE_NAME_HERE 并包含文件的内容。 如果标头再次包含到同一个文件中,则 SOME_UNIQUE_NAME_HERE 从第一次包含标头内容时就已经被定义,并且标头的内容将被忽略。

所有头文件都应该有头保护。 SOME_UNIQUE_NAME_HERE 可以是您想要的任何名称,但按照约定设置为头文件的完整文件名,全部大写,使用下划线表示空格或标点符号。 例如,square.h 将具有标头保护:

square.h:

#ifndef SQUARE_H
#define SQUARE_H

int getSquareSides()
{
    return 4;
}

#endif

甚至标准库头也使用头保护。 如果您要从 Visual Studio 查看 iostream 头文件,您将看到:

#ifndef _IOSTREAM_
#define _IOSTREAM_

// content here

#endif

使用头文件保护来更新之前的示例

让我们回到 square.h 示例,使用带有标头保护的 square.h。 为了获得良好的形式,我们还将向wave.h 添加标头防护。

square.h

#ifndef SQUARE_H
#define SQUARE_H

int getSquareSides()
{
    return 4;
}

#endif

wave.h:

#ifndef WAVE_H
#define WAVE_H

#include "square.h"

#endif

main.cpp:

#include "square.h"
#include "wave.h"

int main()
{
    return 0;
}

预处理器解析所有 #include 指令后,该程序如下所示:

main.cpp:

// Square.h included from main.cpp
#ifndef SQUARE_H // square.h included from main.cpp
#define SQUARE_H // SQUARE_H gets defined here

// and all this content gets included
int getSquareSides()
{
    return 4;
}

#endif // SQUARE_H

#ifndef WAVE_H // wave.h 包含在 main.cpp 中
#define WAVE_H
#ifndef SQUARE_H // square.h 包含在 wave.h 中,SQUARE_H 已从上面定义
#define SQUARE_H // 所以这些内容都没有被包含在内

int getSquareSides()
{
    return 4;
}

#endif // SQUARE_H
#endif // WAVE_H

int main()
{
    return 0;
}

首先,预处理器评估#ifndef SQUARE_H。 SQUARE_H 尚未定义,因此包含#ifndef 到后续#endif 的代码进行编译。 此代码定义了 SQUARE_H,并具有 getSquareSides 函数的定义。

稍后,评估下一个#ifndef SQUARE_H。 这次,SQUARE_H 被定义了(因为上面已经定义了),所以从 #ifndef 到后续 #endif 的代码被排除在编译之外。

头文件保护可防止重复包含,因为第一次遇到保护时,未定义保护宏,因此包含受保护的内容。 过了该点,就会定义保护宏,因此受保护内容的任何后续副本都会被排除。

头文件防护不会阻止标头被包含到不同的代码文件中

请注意,头文件保护的目标是防止代码文件接收受保护标头的多个副本。 根据设计,标头防护不会阻止给定的标头文件被包含(一次)到单独的代码文件中。 这也可能会导致意想不到的问题。 考虑:

square.h:

#ifndef SQUARE_H
#define SQUARE_H

int getSquareSides()
{
    return 4;
}

int getSquarePerimeter(int sideLength); // forward declaration for getSquarePerimeter

#endif

square.cpp:

#include "square.h"  // square.h is included once here

int getSquarePerimeter(int sideLength)
{
    return sideLength * getSquareSides();
}

main.cpp:

#include "square.h" // square.h 也包含在这里一次
#include <iostream>

int main()
{
    std::cout << "a square has " << getSquareSides() << " sides\n";
    std::cout << "a square of length 5 has perimeter length " << getSquarePerimeter(5) << '\n';

    return 0;
}

请注意,main.cpp 和 square.cpp 中均包含 square.h。 这意味着 square.h 的内容将被包含到 square.cpp 和 main.cpp 中一次。

让我们更详细地研究一下为什么会发生这种情况。 当 square.h 包含在 square.cpp 中时,SQUARE_H 被定义到 square.cpp 末尾。 此定义可防止 square.h 再次包含到 square.cpp 中(这是标头防护的要点)。 然而,一旦 square.cpp 完成,SQUARE_H 就不再被视为已定义。 这意味着当预处理器在 main.cpp 上运行时,SQUARE_H 最初并未在 main.cpp 中定义。

最终结果是 square.cpp 和 main.cpp 都获得了 getSquareSides 定义的副本。 该程序将编译,但链接器会抱怨您的程序对标识符 getSquareSides 有多个定义!

解决此问题的最佳方法是将函数定义放入其中一个 .cpp 文件中,以便标头仅包含前向声明:

square.h:

#ifndef SQUARE_H
#define SQUARE_H

int getSquareSides(); // getSquareSides 的前向声明
int getSquarePerimeter(int sideLength); // getSquarePerimeter 的前向声明

#endif

square.cpp:

#include "square.h"

int getSquareSides() // getSquareSides 的实际定义
{
    return 4;
}

int getSquarePerimeter(int sideLength)
{
    return sideLength * getSquareSides();
}

main.cpp:

#include "square.h" // square.h is also included once here
#include <iostream>

int main()
{
    std::cout << "a square has " << getSquareSides() << " sides\n";
    std::cout << "a square of length 5 has perimeter length " << getSquarePerimeter(5) << '\n';

    return 0;
}

现在,当程序编译时,函数 getSquareSides 将只有一个定义(通过 square.cpp),文件 main.cpp 能够调用此函数(即使它位于 square.cpp 中),因为它包含 square.h,该函数具有该函数的前向声明(链接器会将 main.cpp 中对 getSquareSides 的调用连接到 square.cpp 中 getSquareSides 的定义)。

不能避免头文件中的定义吗?

我们通常建议不要在头文件中包含函数定义,但你可能会想知道为什么在头文件中包含函数定义的情况下还需要使用头文件保护。

在以后的课程中,我们将向你展示一些必须将非函数定义放入头文件的情况。例如,C++允许你创建自己的数据类型。这些自定义类型通常在头文件中定义,以便类型定义可以传播到需要使用它们的代码文件中。如果没有头文件保护,一个代码文件可能会包含多个(相同的)给定类型定义的副本,这将被编译器标记为错误。

因此,尽管在本教程系列的当前阶段不严格需要使用头文件保护,但我们正在养成良好的习惯,以便以后不必去养成不良习惯。头文件保护对于确保代码的一致性和可维护性非常重要,特别是在大型项目中。

#pragma once

现代编译器使用 #pragma 预处理器指令支持更简单的替代形式的头文件保护:

#pragma once

// your code here

#pragma once 和头文件保护(header guards)具有相同的目的,即防止头文件被多次包含。使用传统的头文件保护,开发人员负责保护头文件(通过使用预处理器指令 #ifndef#define#endif)。而使用 #pragma once,我们请求编译器来保护头文件。

对于大多数项目来说,#pragma once 工作得很好,许多开发人员现在更喜欢它,因为它更容易使用并且更不容易出错。许多集成开发环境(IDE)也会在通过 IDE 生成的新头文件的顶部自动包含 #pragma once

因为 #pragma once 不是由C++标准定义的,所以可能有一些编译器没有实现它。因此,一些开发团队(如Google)建议使用传统的头文件保护。在本教程系列中,我们将更倾向于使用头文件保护,因为它们是保护头文件的最传统方式。然而,目前大多数编译器都支持 #pragma once,如果你愿意使用 #pragma once,在现代C++中通常也被接受。

总结

头文件保护的目的是确保给定头文件的内容不会被复制超过一次到任何单个文件中,以防止重复定义。

重复的声明是可以的,但即使你的头文件只包含所有的声明而没有定义,包含头文件保护仍然是一种最佳实践。

需要注意的是,头文件保护不会防止头文件的内容被复制(一次)到不同的项目文件中。这是好事,因为我们经常需要从不同的项目文件中引用给定头文件的内容。

原创文章,作者:jkhxw,如若转载,请注明出处:https://www.jkhxw.com/cpp-header-guards/

(0)
上一篇 2023年10月9日 下午11:58
下一篇 2023年10月10日

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注