为什么要使用头文件保护?

16

维基百科所述,包含保护(Include guards)用于防止在编译时加载相同的代码两次。

为什么我的编译器(GCC)不能检测到它正在加载相同的代码,并具有明智的默认行为?


5
有些人会故意重复包含同一个文件(第二次包含会产生实际效果)。这是可行的且合法的,所以更改此行为将会破坏现有代码。 - user743382
7个回答

18

只是因为您可能希望编译器将该文件加载两次。

请记住,#include指令只是加载文件并将其内容放置在指令的位置。这个文件可能是头文件,但也可能是有用且经常使用的源代码片段。

大多数现代编译器会对#pragma once做出正确的反应,实现您想要的效果。不过,请记住,这是一种编译器扩展,不包括在语言规范中,通常最好坚持使用包含保护方式-这样您就可以确信它适用于每个编译器和任何情况。


1
关于#pragma once:它并不是100%可靠的,因此您仍然需要使用include guards。 - James Kanze
@JamesKanze - 你说的“不是100%可靠”是什么意思? - Spook
2
有时候无法检测两个包含文件是否引用了同一个文件。这种情况不会出现在简单的独立系统中(至少我不知道),但是一旦开始进行网络连接,就可能会发生(实际上很少)。委员会曾经讨论过将#pragma once作为标准,但最终拒绝了它,因为它不能(可靠地)实现。 - James Kanze
@JamesKanze 好的,值得注意。 - Spook
@JamesKanze - 噢,你的意思是同一文件通过不同路径访问可能会引起混淆? #include "test.h" #include "c:\source\test.h" #include "c:/symbolic/link/to/test.h"。我可以看出这可能是个问题... - Roddy
2
@Roddy 不仅仅是符号链接的问题——至少在Unix下,有办法解决这个问题。但当一个导入的文件系统被挂载在两个不同的位置时。(奇怪的是,这种情况比人们预期的要更常见。) - James Kanze

12

为什么我的编译器(GCC)不能检测到它正在加载两次相同的代码

实际上,它是可以检测到的(或者说,处理头文件包含的预处理器可以检测到)。你可以使用一个非标准但广泛支持的扩展,而不是使用include guards。

#pragma once

为了表示该标题仅应包含一次。

并且具有明智的默认行为吗?

该语言默认情况下不指定此行为,主要是因为该语言可以追溯到跟踪包含的标题可能过于昂贵的时期,部分原因是有时您确实希望多次包含头文件。例如,标准的<assert.h>头文件可以通过定义或未定义NDEBUG来重新包含,以更改assert宏的行为。


#pragma once 是一个预处理器特性,而不是编译器特性。 - datenwolf
@datenwolf:确实,如果你想将前几个翻译阶段视为“预处理”,那就可以这样做。 - Mike Seymour
1
@datenwolf:这与基本编译的问题无关,所有重要的是它是翻译过程的一部分,我们可以称之为“编译”,但我已经添加了一条注释,可能会让一些学究满意。 - Mike Seymour
1
@datenwolf 是由谁指定的?标准规定了程序翻译的9个步骤。术语“预处理器”是第1到第6步的非正式表达方式(不是精确的)。 - James Kanze
1
@datenwolf 什么编译器可以将预处理器作为独立进程运行?(当然,这并不重要。g++ 和许多其他编译器都将前端、优化器和后端作为单独的进程运行。) - James Kanze
显示剩余3条评论

9

由于存在奇怪的边缘情况,重新包含文件是有用的。

假设您有一个名为mymin.h#include文件,内容如下:

// mymin.h : ugly "pseudo-template" hack
MINTYPE min(MINTYPE a, MINTYPE b)
{
   return (a < b) ? a : b;
}

您可以像这样做:
#define MINTYPE int
#include "mymin.h"

#define MINTYPE double
#include "mymin.h"

现在,你有两个不同类型的min重载函数,这是http://thedailywtf.com/的好例子。谁需要模板呢?;-)
请注意,许多现代预处理器支持#pragma once,这是一种更好的方式来实现与包含保护相同的效果。然而,它不幸地是非标准的。

在这种情况下,为什么不在包含头文件之前#define A和B两个变量呢? - phuclv
@LưuVĩnhPhúc_ 我确实警告过这是人为的... 它只是为了说明每次重新include一个文件可能会产生不同的效果。 - Roddy
@LưuVĩnhPhúc - 我已经更改了这个“示例”。但这仍然是一个“不要在家里尝试”的情况。 - Roddy
我真的很喜欢这种“模板”制作方式,这样做有什么不好吗?话虽如此,我认为你可以通过基于每种类型定义一个包含保护来使其更加安全(例如MINTYPE_H_int)。 - user4945014

4
为什么我的编译器(GCC)不能检测到它正在加载相同的代码两次,并具有合理的默认行为?
因为不是编译器进行包含处理,而是由预处理器完成的,预处理器本质上是一个文本转换引擎。对于文本转换引擎来说,如果在处理一段文本时多次出现相同的包含,这是完全有意义的。
让我们花点时间理解一下:编译器不处理 #include。这就是使编译器无法对符号重定义做出明智决策的原因。
其他语言将模块实现为语言的一部分,在这些语言中,事物不会被处理为文本替换,编译器实际上具有有关导入语义的知识。

0
为什么我的编译器(GCC)不能检测到它正在加载相同的代码,并具有合理的默认行为呢?
因为那样它就不是一个C编译器了。该语言的规范指定#include创建文本包含,对规范进行不同的操作将破坏有效的代码。
显而易见的后续问题,“我们能改变C标准吗?”仍然必须找到某种方式来避免破坏现有有效代码。
编译器可以合法地执行的一件事情是,在没有指示它是有意的情况下多次包含非空(在处理#ifdef等之后)文件时发出警告。如果你足够有动力,也许你可以为你喜欢的编译器准备一个合适的补丁?
顺便说一句,一旦你不得不提出“相同代码”的良好健壮定义,这个问题就会变得非常困难。

0

包含保护指令可以防止符号重定义和多次包含相同的文件。

编译器需要这个机制,因为出于明显的原因,它不包含分析和决定要考虑哪个代码版本的机制。想象一下,如果两个不同的头文件中有相同的函数签名,只是返回类型不同,会发生什么情况。

假设内容完全相同,只是从多个头文件中包含,编译器将需要额外的计算能力和内存来跟踪已经包含的代码。

因此,这样做容易出错且效率低下。


-2
即使编译器决定这样做,它也需要跟踪大量的文件,并且很多时候(正如itwasntpete所评论的那样),编译器无法区分实际代码和头文件。

4
没什么意义。它必须跟踪相同数量的#define来进行头文件保护... - Roddy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接