【问题标题】:Header file included only once in entire program?头文件在整个程序中只包含一次?
【发布时间】:2015-09-01 14:29:18
【问题描述】:

我知道这是一个常见问题,但我仍然无法完全理解它。

在从多个不同的源文件和头文件生成的C或C++程序中,当使用头保护时,每个头文件是否会在整个代码中只包含一次?

之前有人告诉我,一个头文件(带有包含保护)只会在一个翻译单元中包含一次,但在整个代码中会包含多次。这是真的吗?

如果它在整个代码中只包含一次,当一个文件希望包含它并且预处理器检测到它已经被包含时,希望使用它的那个文件如何知道它在之前包含的代码中的下落?

【问题讨论】:

  • “一个头文件(带有包含保护)只会在一个翻译单元中包含一次,但在整个代码中会包含多次。这是真的吗?” 是的。不是每个程序一次,而是(最多)每个翻译单元一次。
  • 使用包含守卫,即使在一个翻译单元中,标题仍然可以包含多次 - 但大部分(守卫中的部分)将在第一次之后被跳过。预处理器不关心它以前包含在哪里......只关心是否已经定义了守卫的宏(如果守卫设置正确,第一次包含它时会发生这种情况)。
  • @Engineer999:如果回答有帮助,请不要忘记接受。这就是这个网站的运作方式。
  • @MichaelGaskill 我认为从问题和答案的详细程度来看应该很明显,这不是重复的。

标签: c++ c c-preprocessor header-files


【解决方案1】:

这是过程:

source           header   source header header
   \           /        \   |      /   /
    \         /          \  |     /   /
  PREPROCESSOR            PREPROCESSOR
       |                      |
       V                      V
 preprocessed code      preprocessed code
       |                      |
    COMPILER               COMPILER
       |                      |
       V                      V
  object code              object code
             \            /
              \          /
               \        /
                 LINKER
                   | 
                   V
               executable

预处理

#include 是第一步。它指示预处理器处理指定的文件,并将结果插入到输出中。

如果A 包含BC,并且B 包含C,则A 的预处理器输出将包含C 的处理文本两次。

这是一个问题,因为它会导致重复声明。一种补救方法是使用预处理器变量跟踪是否包含源代码(也称为标头保护)。

#ifndef EXAMPLE_H
#define EXAMPLE_H

// header contents

#endif

第一次,EXAMPLE_H 未定义,预处理器将评估 ifndef/endif 块中的内容。第二次,它将跳过该块。所以处理后的输出变化,定义只包含一次。

这很常见,以至于一些编译器实现了一个非标准指令,该指令更短并且不需要选择唯一的预处理器变量:

#pragma once

// header contents

您可以确定您希望 C/C++ 代码的可移植性,以及使用哪个标头保护。

标头守卫将确保每个标头文件的内容在翻译单元的预处理代码中最多出现一次。

编译

编译器从您预处理的 C/C++ 生成机器代码。

通常,头文件仅包含声明,而不包含实际定义(也称为实现)。编译器为当前缺少定义的任何内容都包含一个符号表。

链接

链接器组合目标文件。它将定义(又名实现)与对符号表的引用进行匹配。

可能是两个目标文件提供了定义,链接器会取一个。如果您将可执行代码放在标题中,就会发生这种情况。这在 C 中通常不会发生,但在 C++ 中却经常发生,因为模板。

标头“代码”,无论是声明还是定义,都包含在所有目标文件中多次,但链接器将所有这些合并在一起,因此它仅在可执行文件中出现一次。 em>(我不包括多次出现的内联函数。)

【讨论】:

  • 很好的回复,但你不应该说“通常,头文件只包含声明,而不是实际的实现。”因为定义通常与 c/c++ 中的实现同义?
  • @BryanShaw,已修复。
  • 很棒的回复!正是我要找的!
  • 优秀的答案!很好解释,谢谢!
【解决方案2】:

带有适当include guards 的头文件将仅包含每个翻译单元一次。严格来说,它可能被多次包含,但在后续包含时,预处理器#ifndef#endif 之间的部分将被跳过。如果操作正确,这应该是文件的全部(或大部分)。

一个翻译单元通常对应一个“源文件”,尽管一些晦涩的实现可能使用不同的定义。如果单独编译的源文件包含相同的头文件,则预处理器无法知道另一个文件已经包含它,或者任何其他文件是同一项目的一部分。

请注意,当您将多个源文件(翻译单元)链接到一个二进制文件中时,如果标头不仅仅包含声明、模板、函数,您可能会遇到多个定义的问题标记为inline 的定义或静态变量定义。为避免这种情况,您应该在头文件中声明函数并将它们定义在单独的源文件中,然后将其与其他源文件链接在一起。

【讨论】:

  • 是的,这个静态声明很重要。在 linux 源代码中,头文件中定义了许多“静态内联”函数。即使它们在多个目标文件中使用,由于“静态”性质,它在链接过程中不会给我们带来“多重定义”错误。
【解决方案3】:

“头文件”实际上是在编译开始之前由预处理器插入的。只需将其视为“替换”其#include 指令即可。

守卫……

#ifndef MY_HEADER_H
#define MY_HEADER_H

....

#endif

... 在替换后执行。因此,标头实际上可能包含多次,但文本的“受保护”部分仅由预处理器传递给编译器一次。

因此,如果标头中有任何代码生成定义,它们当然会被包含在编译单元(又名“模块”)的目标文件中。如果同一个header在多个模块中是#includeded,则会出现多次。

对于static 定义,这完全没有问题,因为这些在模块之外是不可见的(又名文件范围)。对于程序全局定义,这是不同的,将导致“多个定义”错误。

注意:这主要用于C。对于 C++,存在显着差异,因为类等增加了允许多个全局对象/何时允许的额外复杂性。

【讨论】:

  • 你也可以提到非标准的#pragma once,它的存在值得知道
  • @dlavila:我不是故意的。一个人不应该依赖它,所以无论如何你必须给守卫做广告。此外,这可能会挑战像 boost 这样故意使用多个包含的库。 (我不会一直坚持这个标准,但链接一次的好处太少,而且会造成很多麻烦)。
  • 无论如何都值得知道它的存在,而不是作为替代品,但有时它可能非常有用,例如:如果您确定它受到支持(您公司的 SWE 人员告诉您)并且您正在工作在一个有数千个标头的项目中(编译时间可以加快很多)。
  • 在C++的情况下,我们不应该只将模板类和函数定义放在头文件中吗?那样的话,程序中的同一个函数会不会有多个定义,这是不允许的吗?
  • @Engineer999:这就是为什么我写的答案主要是针对 C 的。前段时间我用 C++ 编程时,我只记得它更复杂,例如使用内联方法或常量 - 这些与 C 不同。用一个问题涵盖这两种语言总是有问题的。它们是不同的语言。如果您无法推断我的答案,最好删除 C++ 标记并再次询问 C++。
【解决方案4】:

每个翻译单元将包含一次头文件,是的。每个程序可以多次包含它,因为每个翻译单元都在编译过程中单独处理。它们在链接过程中组合在一起形成一个完整的程序。

【讨论】:

  • 如果我们在头文件中有函数定义会发生什么。整个程序中会不会有很多同一个函数的定义是不允许的?
  • @Engineer999 函数定义通常不包含在头文件中,内联函数除外
  • 如果函数是在头文件的类主体内定义的(当然是 C++),则函数不是隐式内联的。那么多重定义如何处理呢?
  • @Engineer999 内联函数的设计目的是在多次包含时不会引起问题 - 要么它们在最终输出中根本没有定义,要么链接器知道除了其中一个之外都丢弃.
猜你喜欢
  • 2020-02-09
  • 2011-07-17
  • 2021-05-10
  • 1970-01-01
  • 1970-01-01
  • 2011-03-20
  • 2010-11-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多