头文件只包含在整个程序中一次?

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

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

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

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

这是一个过程:

source header source header header \ / \ | / / \ / \ | / / PREPROCESSOR PREPROCESSOR | | VV preprocessed code preprocessed code | | COMPILER COMPILER | | VV 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 ++中经常发生。

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

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

守卫 …

 #ifndef MY_HEADER_H #define MY_HEADER_H .... #endif 

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

因此,如果标题中有任何代码生成定义,它们 – 当然 – 将包含在编译单元的对象文件中(也称为“模块”)。 如果相同的标题是多个模块中的#include ded,它们将多次出现。

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

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

每个翻译单元只包含一个带有相应包含警卫的头文件。 严格地说,它可能被包含多次,但是后续包含中将跳过预处理器#ifndef#endif之间的部分。 如果正确完成,则应该是文件的全部(或大部分)。

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

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

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