章节目录

头文件保护

本节阅读量:

重复定义问题

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

1
2
3
4
5
6
7
int main()
{
    int x; // 变量 x 的定义
    int x; // 编译失败: 重复定义

    return 0;
}

类似地,多次定义相同函数的程序也将导致编译错误:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <iostream>

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

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

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

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

考虑以下示例:

square.h:

1
2
3
4
int getSquareSides()
{
    return 4;
}

wave.h:

1
#include "square.h"

main.cpp:

1
2
3
4
5
6
7
#include "square.h"
#include "wave.h"

int main()
{
    return 0;
}

此程序看似正常,但实际上无法编译!原因如下。首先,main.cpp包含了square.h,这会将函数getSquareSides的定义复制到main.cpp中。其次,main.cpp包含了wave.h,而wave.h又间接包含了square.h。这会将square.h的内容(包括函数getSquareSides的定义)先复制到wave.h中,然后再复制到main.cpp中。

因此,在解析所有#include之后,main.cpp最终如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
int getSquareSides()  // from square.h
{
    return 4;
}

int getSquareSides() // from wave.h (via square.h)
{
    return 4;
}

int main()
{
    return 0;
}

重复定义导致了编译错误。每个文件可以单独编译通过。然而,由于main.cpp包含了两次square.h的内容,所以出现了这个问题。如果wave.h需要getSquareSides(),同时main.cpp又需要wave.h和square.h,该如何解决此问题?


头文件保护

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

1
2
3
4
5
6
#ifndef SOME_UNIQUE_NAME_HERE
#define SOME_UNIQUE_NAME_HERE

// 这里放置你的声明

#endif

当包含此头文件时,预处理器检查SOME_UNIQUE_NAME_HERE是否已经定义过。如果是第一次包含该头文件,SOME_UNIQUE_NAME_HERE不会被定义。因此,预处理器会定义SOME_UNIQUE_NAME_HERE并包含文件的内容。如果该头文件再次被包含到同一文件中,SOME_UNIQUE_NAME_HERE已经被定义,头文件的内容将被忽略(由于#ifndef)。

所有头文件都应有头文件保护。SOME_UNIQUE_NAME_HERE可以取任何名称,但根据惯例,通常设置为头文件的完整文件名,使用大写字母,用下划线替代空格或标点。例如,square.h将具有如下头文件保护:

square.h:

1
2
3
4
5
6
7
8
9
#ifndef SQUARE_H
#define SQUARE_H

int getSquareSides()
{
    return 4;
}

#endif

标准库头文件也使用头文件保护。比如,Visual Studio中的iostream头文件,将看到:

1
2
3
4
5
6
#ifndef _IOSTREAM_
#define _IOSTREAM_

// 对应的代码内容

#endif

使用头文件保护更新开头示例

现在回到square.h的示例,使用带有头文件保护的square.h。为了更完善,我们还在wave.h中添加头文件保护。

square.h

1
2
3
4
5
6
7
8
9
#ifndef SQUARE_H
#define SQUARE_H

int getSquareSides()
{
    return 4;
}

#endif

wave.h:

1
2
3
4
5
6
#ifndef WAVE_H
#define WAVE_H

#include "square.h"

#endif

main.cpp:

1
2
3
4
5
6
7
#include "square.h"
#include "wave.h"

int main()
{
    return 0;
}

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

main.cpp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Square.h 引入
#ifndef SQUARE_H
#define SQUARE_H

int getSquareSides()
{
    return 4;
}

#endif // Square.h 引入到此为止

#ifndef WAVE_H // wave.h引入
#define WAVE_H
#ifndef SQUARE_H // 从wave.h引入的square.h, SQUARE_H 在之前已定义
#define SQUARE_H // 所以接下来的getSquareSides()不会被编译

int getSquareSides()
{
    return 4;
}

#endif // 
#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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#ifndef SQUARE_H
#define SQUARE_H

int getSquareSides()
{
    return 4;
}

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

#endif

square.cpp:

1
2
3
4
5
6
#include "square.h"  // square.h 这里被包含

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

main.cpp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#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.cpp处理完成,SQUARE_H就不再被认为是已定义的。这意味着当预处理器处理main.cpp时,SQUARE_H仍然是未定义的。

最终结果是square.cpp和main.cpp都获得了getSquareSides定义的副本。该程序可以编译通过,但链接时会报错,因为程序中存在getSquareSides的多个定义!

解决此问题的最佳方法是将函数定义放在.cpp文件中,头文件只包含前向声明:

square.h:

1
2
3
4
5
6
7
#ifndef SQUARE_H
#define SQUARE_H

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

#endif

square.cpp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include "square.h"

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

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

main.cpp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#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中),因为main.cpp引用了square.h,其中包含对应函数的前向声明(链接器会将main.cpp中对getSquareSides的调用连接到square.cpp中的getSquareSides定义)。


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

通常不要在头文件中包含函数定义。如果这么做了,为什么还需要头文件保护呢?

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

因此,在本节教程中,严格来说没必要在此时配置头文件保护,之所以如此是出于建立良好习惯的需要。


#pragma once

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

1
2
3
#pragma once

// your code here

#pragma once与头文件保护具有相同的用途:避免头文件被多次包含。使用传统的头文件保护,开发人员负责保护头文件(通过使用预处理器指令#ifndef、#define和#endif)。使用#pragma once,我们请求编译器保护头文件。它究竟如何做到,取决于实现的细节。

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

由于#pragma once不是由C++标准定义的,因此一些编译器可能不会实现它。因此,一些开发公司(如Google)建议使用传统的头文件保护。在本教程系列中,介绍头文件保护,因为它们是保护头文件的最传统方法。

目前对#pragma once的支持是相当普遍的,如果使用#pragma once,在现代C++中是普遍接受的。


总结

头文件保护旨在确保给定头文件的内容不会多次复制到单个文件中,防止重复定义。

重复声明是可以的——但即使头文件只由声明(没有定义)组成,也最好使用头文件保护。

请注意,头文件保护不会阻止将头文件的内容复制到多个文件中。


2.10 头文件

上一节

2.12 设计第一个程序

下一节