【问题标题】:Why include guards?为什么包括警卫?
【发布时间】:2016-04-13 09:52:45
【问题描述】:

包含保护,定义为here,用于防止在编译时两次加载相同的代码。

为什么我的编译器 (GCC) 无法检测到它两次加载相同的代码并具有合理的默认行为?

【问题讨论】:

  • 有些人会故意将同一个文件包含两次(其中第二次包含有实际效果)。这行得通,这是有效的,所以改变它会破坏现有的代码。

标签: c++ c gcc compiler-construction preprocessor


【解决方案1】:

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

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

大多数现代编译器都会对 #pragma once 做出反应,完全按照您的意愿行事。但请记住,这是一个未包含在语言规范中的编译器扩展,并且坚持包含保护通常是一个好主意 - 您可以肯定,它适用于每个编译器,并且在任何情况下都适用。

【讨论】:

  • Re #pragma once:它不是 100% 可靠的,所以你仍然需要包含警卫。
  • @JamesKanze - “不是 100% 可靠”你想说什么?
  • 有时无法检测两个包含是否引用同一个文件。它们不会出现在简单、独立的系统中(至少不是我所知道的),但是一旦您开始联网,就会出现(实际上确实如此,尽管很少)。委员会实际上讨论了将 #pragma once 设为标准,并拒绝了它,因为它不(可靠)可实施。
  • @JamesKanze 好的,可能值得注意。
  • @Roddy 不仅来自符号链接——至少在 Unix 下,有一些方法可以解决这个问题。但是当一个导入的文件系统安装在两个不同的地方时。 (奇怪的是,这种情况发生的频率超出了人们的预期。)
【解决方案2】:

为什么我的编译器 (GCC) 无法检测到它两次加载相同的代码

它可以(或者,学究式地,处理标头包含的预处理器可以)。您可以使用非标准但广泛支持的扩展,而不是使用包含守卫

#pragma once

表示此标头应仅包含一次。

并且有一个合理的默认行为?

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

【讨论】:

  • #pragma once 是预处理器功能,而不是编译器功能。
  • @datenwolf:确实,如果您想将前几个翻译阶段视为“预处理”。
  • @datenwolf:这与这个关于基本编译的问题无关,重要的是它是翻译过程的一部分,我们不妨粗略地称之为“编译”,但我添加了一个请注意,这可能会让一些学究感到高兴。
  • @datenwolf 由谁?该标准总共规定了程序翻译的9个步骤。术语“预处理器”是前 6 个的口语表达(不准确)。
  • @datenwolf 什么编译器将预处理器作为单独的进程运行? (当然,这并不重要。g++,可能还有很多其他编译器,将前端、优化器和后端作为单独的进程运行。)
【解决方案3】:

因为在某些奇怪的极端情况下,重新包含文件很有用。

人为的丑陋示例:假设您有一个像这样的#include 文件mymin.h

// 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,这是实现与包含防护相同效果的更好方法。但是,不幸的是,它是非标准的。

【讨论】:

  • 在这种情况下,为什么不#defineA 和B 都包含标题?
  • @LưuVĩnhPhúc _ 我确实警告过这是人为的......这只是为了说明每次重新include文件可能会产生不同的效果。
  • @LưuVĩnhPhúc - 我已经更改了“示例”。但这仍然是“不要在家里尝试这个”的情况。
  • 我真的很喜欢这种制作“模板”的方式是不是很糟糕?也就是说,我认为您可以通过根据每种类型定义一个包含防护来使其更安全(例如MINTYPE_H_int)。
【解决方案4】:

为什么我的编译器 (GCC) 无法检测到它两次加载相同的代码并具有合理的默认行为?

因为不是编译器在进行包含处理。它是由本质上是一个文本转换引擎的预处理器完成的。对于文本转换引擎,如果在处理一段文本时多次出现相同的包含,则非常有意义。

让我们沉思一下:编译器不处理#includes。这就是编译器无法对符号重新定义做出明智决定的原因。

其他语言将模块实现为该语言的一部分,在这些语言中,不会将内容作为文本替换处理,编译器实际上了解导入语义。

【讨论】:

    【解决方案5】:

    包含防护可防止符号重新定义并多次包含相同的文件。

    编译器需要这种机制,因为显而易见的原因,它不包括分析和决定要考虑哪个代码版本的机制。想想如果相同的函数签名在两个不同的头文件中只是返回类型不同会发生什么。

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

    因此它容易出错且效率低下

    【讨论】:

      【解决方案6】:

      为什么我的编译器 (GCC) 无法检测到它两次加载相同的代码并具有合理的默认行为?

      因为那样它就不是 C 编译器了。指定语言使得#include 创建文本嵌入,并且执行与规范不同的操作会破坏有效代码。

      明显的后续问题,“我们可以更改 C 标准吗?”仍然必须找到一些方法来避免对现有有效代码的同样破坏。

      编译器可以合法地做的一件事是当一个非空的(在处理#ifdef等之后)文件被多次包含而没有一些时发出一个警告表明这是故意的。如果你有足够的动力,也许你可以为你最喜欢的编译器准备一个合适的补丁?

      顺便说一句,一旦你必须对“相同代码”提出一个很好的稳健定义,你就会发现问题变得非常困难。

      【讨论】:

        【解决方案7】:

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

        【讨论】:

        • 毫无意义。它必须为标题保护跟踪相同数量的#defines...
        猜你喜欢
        • 1970-01-01
        • 2011-06-19
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-07-08
        • 2013-08-12
        相关资源
        最近更新 更多