头文件保护

头文件保护

重复定义问题

之前我们提到变量或函数标识符只能有一个定义(单一定义规则)。因此,定义变量标识符超过一次的程序会导致编译错误:

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;
}

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

作者注 在即将到来的例子中,我们将在头文件中定义一些函数。你通常不应该这样做。 我们在这里这样做,因为这是使用我们已经介绍过的功能来演示一些概念的最有效方式。

头文件重复包含的问题

考虑以下学术示例: 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包含square.h,将函数getSquareSides的定义复制到main.cpp中。然后main.cpp包含wave.h,它本身包含square.h。这将square.h的内容(包括函数getSquareSides的定义)复制到wave.h中,然后被复制到main.cpp中。

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

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

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

int main()
{
    return 0;
}

重复定义和编译错误。每个文件单独都是可以的。然而,因为main.cpp最终包含了square.h的内容两次,我们遇到了问题。如果wave.h需要getSquareSides(),并且main.cpp需要wave.h和square.h,你将如何解决这个问题?

头文件保护机制

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

#ifndef SOME_UNIQUE_NAME_HERE
#define SOME_UNIQUE_NAME_HERE

// 你的声明(和某些类型的定义)在这里

#endif

当这个头文件被#include时,预处理器将检查SOME_UNIQUE_NAME_HERE是否已在此翻译单元中定义。如果这是我们第一次包含头文件,SOME_UNIQUE_NAME_HERE将未定义。因此,它#define SOME_UNIQUE_NAME_HERE并包含文件的内容。如果头文件再次被包含到同一个文件中,SOME_UNIQUE_NAME_HERE将已经从第一次包含头文件的内容中定义,并且头文件的内容将被忽略(感谢#ifndef)。 所有的头文件都应该有头文件保护。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_

// 内容在这里

#endif

高级读者 在大型程序中,可能有两独立的头文件(从不同的目录中包含)最终具有相同的文件名(例如directoryA\config.h和directoryB\config.h)。如果仅使用文件名作为包含保护(例如CONFIG_H),这两个文件可能最终使用相同的保护名称。如果发生这种情况,任何直接或间接包含两个config.h文件的文件都不会接收到要包含的头文件的内容。这可能会导致编译错误。 由于保护名称冲突的可能性,许多开发人员建议在头文件保护中使用更复杂/独特的名称。一些好的建议是PROJECT_PATH_FILE_H、FILE_LARGE-RANDOM-NUMBER_H或FILE_CREATION-DATE_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:

// 从main.cpp包含的square.h
#ifndef SQUARE_H // square.h从main.cpp包含
#define SQUARE_H // 在这里定义SQUARE_H

// 包含所有这些内容
int getSquareSides()
{
    return 4;
}

#endif // SQUARE_H

#ifndef WAVE_H // wave.h从main.cpp包含
#define WAVE_H
#ifndef SQUARE_H // wave.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); // getSquarePerimeter的前向声明

#endif

square.cpp:

#include "square.h"  // 这里包含square.h一次

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;
}

请注意,square.h从main.cpp和square.cpp都被包含。这意味着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一次
#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

// 你的代码在这里

#pragma once的作用与头文件保护相同:避免头文件被多次包含。与传统头文件保护相比,开发者负责保护头文件(通过使用预处理器指令#ifndef、#define和#endif)。有了#pragma once,我们请求编译器保护头文件。它如何做到这一点是实现细节。

高级读者 有一个已知情况,#pragma once通常会失败。如果一个头文件被复制,以至于它在文件系统中的多个地方存在,如果两个副本的头文件都被包含,头文件保护将成功地消除相同的头文件,但#pragma once不会(因为编译器不会意识到它们实际上是相同的内容)。

对于大多数项目,#pragma once工作得很好,许多开发人员现在更喜欢它,因为它更容易且不易出错。许多IDE还会自动在IDE生成的新头文件顶部包含#pragma once。

警告 #pragma指令旨在供编译器实现者用于他们想要的任何目的。因此,支持哪些pragma以及这些pragma的含义完全取决于实现。除了#pragma once之外,不要指望在一个编译器上工作的pragma在另一个编译器上得到支持。

因为#pragma once没有被C++标准定义,所以一些编译器可能不实现它。因此,一些开发机构(例如Google)建议使用传统的头文件保护。在本教程系列中,我们将倾向于头文件保护,因为它们是保护头文件的最传统方式。然而,对#pragma once的支持在这一点上相当普遍,如果你愿意使用#pragma once,那通常在现代C++中是被接受的。

总结

头文件保护旨在确保给定头文件的内容不会被复制到任何单个文件中超过一次,以防止重复定义。 重复声明是可以的——但即使你的头文件由全部声明(没有定义)组成,包含头文件保护仍然是最佳实践。

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

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"