2.12 - 头文件防卫式声明
Key Takeaway
- 由于头文件防范不能防止头文件被包含到不同的文件,头文件中的定义可能会导致链接器报告函数重定义。
- 不可能完全避免头文件中的定义,毕竟类是需要定义在头文件中的。
- 对于不包含定义的头文件,虽然没必要使用头文件防范,但这其实是一个好习惯(尤其考虑到后面可能对头文件进行的改动)
- 处于兼容性的考虑,优先使用头文件防范而非
#pragma once
重复定义问题
在2.7 - 前向声明和定义中我们介绍了变量和函数的标识符只能被定义一次,即单一定义规则。因此,如果一个程序包含了对某个标识符的多次定义,将会引起编译错误 :
1 2 3 4 5 6 7 |
|
类似地,如果一个函数在程序中被定义了多次,也会导致编译器报错:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
尽管这些问题修复起来并不难(移除重复定义即可),但是对于使用头文件的情况来说,如果头文件中存在定义,那么它很有可能会被重复地包含到代码中。尤其是一个头文件中还包括其他头文件的情况(很常见)。
请考虑下面这个例子:
square.h | |
---|---|
1 2 3 4 5 |
|
geometry.h | |
---|---|
1 |
|
main.cpp | |
---|---|
1 2 3 4 5 6 7 |
|
这个程序看上去没什么问题,但实际上却不能编译!原因如下:首先, main.cpp 文件 #includes
了 square.h,因此会把函数 getSquareSides_into
的定义拷贝到了 main.cpp。main.cpp 文件同时还包含了 #includes
geometry.h 进而 #includes
了 square.h 。这样 square.h 的定义(包含了 getSquareSides
的定义)也被拷贝到了 geometry.h,进而也被拷贝到了 main.cpp。
因此,在解析了的全部的头文件后, main.cpp 的内容变成了下面这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
单独看每个函数都没有问题,不过,由于最终的结果等效于 _main.cpp_
包含了两次 square.h,所以才遇到了上述问题。 如果 geometry.h 需要 getSquareSides()
而且 main.cpp 需要 geometry.h 和 square.h,那么应该如何解决上述问题呢?
头文件防卫式声明
好消息是,我们可以使用头文件防范 header guards(或 include 防范)技术来避免上述问题。头文件防范其实是一种条件编译,它的形式如下:
1 2 3 4 5 6 |
|
当头文件被包含时,预处理器会检查 SOME_UNIQUE_NAME_HERE
是否已经被定义过。如果没有说明这是第一次引入该头文件,SOME_UNIQUE_NAME_HERE
肯定尚未被定义。此时,该头文件会 #defines 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 |
|
标准库中也使用了头文件防范。如果你去看看 iostream 的头文件,你会发现它是这样的:
iostream | |
---|---|
1 2 3 4 5 6 |
|
扩展阅读
在大型项目中,使用两个来自两个不同目录的同名头文件也是可能的(例如:directoryA\config.h
和 directoryB\config.h
)。如果头文件防范只利用了文件名,那么这两个头文件的头文件防范应该也是一样的(例如:CONFIG_H
)。这样一来,任何同时包含了上述两个头文件的文件,其实在包含第二个的时候是不生效的,这样就有可能导致编译错误。
正是由于存在头文件防范冲突的可能,很多开发者建议使用更复杂/独特的方式来构建相关的宏。例如:<PROJECT>_<PATH>_<FILE>_H
、<FILE>_<LARGE RANDOM NUMBER>_H
或者 <FILE>_<CREATION DATE>_H
。
使用头文件防范更新之前的例子
再回到 square.h 的例子,使用头文件防护来处理 square.h 。为了保持一致,我们在 geometry.h 中也添加了头文件夹防护。
square.h | |
---|---|
1 2 3 4 5 6 7 8 9 |
|
geometry.h | |
---|---|
1 2 3 4 5 6 |
|
main.cpp | |
---|---|
1 2 3 4 5 6 7 |
|
在预处理器解析全部头文件后,程序内容变成了下面这样:
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 29 |
|
从上面的例子可以看出,当第二次包含 square.h 的时候,由于 SQUARE_H 已经被定义过了。因此 getSquareSides
函数只会被包含一次。
头文件防范不能防止头文件被包含到不同的文件
头文件防范的目标是防止头文件被多次包含。从设计上来讲,它并不能保护头文件被包含到多个代码文件中(每个文件只能包含一次)。这可能导致难以预料的问题。
考虑如下情况:
square.h | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 |
|
square.cpp | |
---|---|
1 2 3 4 5 6 |
|
main.cpp | |
---|---|
1 2 3 4 5 6 7 8 9 10 |
|
注意 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 | |
---|---|
1 2 3 4 5 6 7 |
|
square.cpp | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 |
|
main.cpp | |
---|---|
1 2 3 4 5 6 7 8 9 10 |
|
现在, 程序在编译的时候,getSquareSides
函数只有一个定义了(来自 square.cpp ),因此链接器不会再报错。 main.cpp 可以调用该函数 (即使它的定义在 square.cpp 中) ,因为它包含了 square.h(包含该函数的声明),链接器会把 main.cpp 中对 getSquareSides
函数的调用关联到 square.cpp 中 getSquareSides
函数的定义。
不把定义放在头文件中不就行了?
之前我们说过,不要把函数的定义放在头文件中。所以你可能会问,那为什么还要使用头文件防范呢?毕竟它要解决的问题从一开始就应该避免。
其实,后面我们会向你展示一些非函数定义被放置在头文件中的例子。例如,C++ 允许你创建自定义类型。这些自定义类型通常被定义在头文件中,这样才能让对应的源文件使用这些定义。如果不使用头文件防范,则它们很可能会被多次拷贝,导致编译器报错。
因此,即便教程看到这里的时候你还没有必要使用头文件防范,能够从现在就养成习惯还是很好的。
#pragma once
现代编译器支持一种更简单的处理方法,可以使用 #pragma
指令来代替头文件防范:
1 2 3 |
|
#pragma once
的功能和头文件防范是一样的,而且更加简短且不易出错。在大多数的项目中,#pragma once
都可以很好的工作,因此很多开发者都倾向于使用它而不是头文件防范。不过,#pragma once
并不是 C++ 语言的一部分 (而且可能永远都不会成为 C++ 的一部分,因为没办法保证在任和情况下它都能正确地工作)
处于兼容性的考虑,我们还是推荐使用传统方式——头文件防范。头文件防范用起来也不复杂而且所有的编译器都支持。
最佳实践
处于兼容性的考虑,优先使用头文件防范而非 #pragma once
。
小结
头文件防范可以确保该头文件的内容被包含到某个文件时,其内容只被拷贝一次,以避免重复定义的问题。
注意,重复声明是可以的,因为多次声明并不会带来任何问题。不过,即使你的头文件中只包含声明,使用头文件防范仍然是最佳实践。
注意,头文件防范不能避免头文件的内容被拷贝到多个不同的文件中。不过,这是好事,因为我们时常需要将头文件包含到多个不同的文件中。