关于 C/C++ 中使用的 header-guards 和 header-file 包含的说明

Posted

技术标签:

【中文标题】关于 C/C++ 中使用的 header-guards 和 header-file 包含的说明【英文标题】:Clarification about the header-guards and header-file inclusion used in C/C++ 【发布时间】:2012-09-01 20:29:32 【问题描述】:

我知道人们建议在头文件中包含头保护,以防止头文件内容被预处理器多次插入到源代码文件中。

但请考虑以下情况:

假设我有文件main.cppstuff.cppcommonheader.h,其中.h 文件有其标头保护。

如果任一.cpp 文件尝试多次包含commonheader.h,则预处理器 将阻止这种情况发生,并且在编译为我们得到的目标代码后,

main.o 包含 commonheader.h 的内容恰好一次。

stuff.o 包含 commonheader.h 的内容恰好一次。

请注意,commonheader 的内容已在文件中重复,但不在同一个 .o 文件中。

那么在链接步骤中会发生什么?由于 .o 文件被融合到一个可执行文件中 我们将不得不第二次确保 commonheader 的内容不被重复。编译器会处理这个吗?如果不是,那么当我们处理巨大的头文件时,这不是一个问题,会导致文件之间的代码重复并导致较大的可执行文件大小。

如果我在问题的任何地方都犯了一些概念上的错误,请纠正我。

【问题讨论】:

你的意思是每个对象文件都得到了commonheader.h的副本? 为什么?我认为,预处理阶段涉及将头文件的内容直接粘贴到源文件中。 是的,#include 宏复制该文件的内容,可能是在通过预处理器运行它之后。 #include 会将文件内容准确地复制到该单词所在的位置! +1 @dutt 【参考方案1】:

通常,您的头文件实际上不应定义任何符号,而应声明它们。所以 commonheader.h 看起来像这样(省略包含守卫):

void commonFunc1(void);
void commonFunc2(void);

在这种情况下,没有问题。如果您在main.cppstuff.cpp 中调用commonFunc1main.ostuff.o 都会知道他们想要链接一个名为commonFunc1 的符号,并且链接器将尝试找到该符号。如果链接器没有找到该符号,则会收到未定义的引用错误。 commonFunc1 的实际定义需要在某个 cpp 文件中。

如果你真的想在你的头文件中定义函数,使用static 这样链接器就看不到它们了。所以你的 commonheader.h 可能看起来像:

static void commonFunc1()

    /* ... do stuff ... */

在这种情况下,链接器不知道commonFunc1,因此不会发生错误。不过,这可能会增加可执行文件的大小;您最终可能会得到两份 commonFunc1 的代码副本。

【讨论】:

嗯...对于函数原型/声明来说,“extern”不是必要的吗? 所以参考你的最后一句话,这就是为什么建议将函数定义和函数声明放在单独的文件中?模板代码呢?所有模板化函数都定义在头文件中,如果不加选择地包含这些文件,我猜这会导致代码膨胀,对吗? 抱歉,我不确定。 C++ 标准模板库非常流行,您会认为在使用它时有很好的解决方案可以避免代码膨胀。也许没关系,因为编译器只会使用你的对象实际需要的代码路径,它可以优化掉所有其他的。 正确,你不能分离模板代码,是的,把它包括在所有地方会导致膨胀。但另一方面,包括到处都是正常的标题也会导致臃肿。你应该注意只包含你需要的东西。在标头中包含尽可能少的内容并使用类型声明,在 .cpp 文件中包含实际类型。 @curiousexplorer:如果一个模板在多个目标文件中使用相同的参数实例化,那么是的,相同的代码将在两个目标文件中可用。但是链接器通常足够聪明,可以丢弃除一个以外的所有副本。您可以尝试在谷歌上搜索“弱符号”或类似的东西。【参考方案2】:

将格雷森的答案扩展到涵盖变量。如果你想在头文件中声明一个变量,你应该使用 extern 关键字。这是处理全局变量的一种方法。

在头文件 global.h 中你这样写:

extern Globals globals;

那么你可以在包括 global.h 在内的任何文件中使用 foo,而你可以在 global.cpp 中编写

#include "globalstype.h"
Globals globals;

请注意,global.cpp 不需要包含 global.h,但是您需要确保将 global.cpp 编译到每个用法中,否则链接器会报错。

【讨论】:

是的,这对变量很重要,因为如果您不小心忘记了头文件中的 extern 关键字,您最终可能会得到两个同名的不同变量。 根据我的经验(msvc,gcc),如果您实际上有两个用法/实例,链接器会抱怨多个定义。【参考方案3】:

头文件通常包含声明性代码而不是定义性代码。那就是他们宣布必须存在一次的东西的存在。宏和内联函数是允许的,并且无论在哪里使用它们都必须重复。

编译器使用声明将未解析的链接(或引用)插入到目标代码中。链接器的工作是通过将引用与单个定义匹配来解析这些链接。

如果你省略包含保护,在一个单个翻译单元中包含多个,你会得到一个编译器错误,因为多个声明现有符号。但是,如果您有一个错误地包含定义的标头,并且该标头包含在多个翻译单元中,则将有多个具有定义的目标文件 - 这反而会导致多个链接器错误 定义

所以当:

extern int b ;  // declaration, may occur in multiple translation units

在头文件中,

int b ; // definition, must occur in only object file.

不是。

不是声明不包含在目标代码中,而是编译器使用它们来创建引用,如果编译器尚未使用定义并已解析它,链接器将解析这些引用。

【讨论】:

【参考方案4】:

是的,这可能是个问题。您最终可能会得到多个定义或冗余副本。

C 在这方面非常简单。你有静态、外部和内联——编译器还定义了几种改变可见性的方法。我认为其他答案已经涵盖了很多内容。

然而,C++ 完全不同。有很多信息,也有隐式定义(例如编译器可能会发出一个复制构造函数或 RTTI)。

在 C++ 中,定义出现在头文件中的可能性更大——考虑模板、类声明中定义的方法等等。 C++ 默认使用单一定义规则。你会想更详细地阅读它,但它基本上指出某些类别的符号可能是多重定义的;根据声明的装饰和位置/范围,在许多情况下,允许链接器假设每个主体(定义)都是相同的,并且可以随意丢弃它遇到的任何副本(在二进制文件中留下一个定义)。所以这确实减少了生成的二进制文件的 size,除非你指定要生成一个副本。

但是,在您的头文件中包含这些定义肯定会增加编译时间、编译每个文件所需的内存和文件、可见的依赖关系,并且会增加在编辑定义时必须重新编译的文件数量。

当然,该语言仍然允许错误的形式,如果您一遍又一遍地反复陈述并在多个翻译中包含必须为每个翻译复制的定义,也不会抱怨。那么你肯定会得到很多臃肿的结果。

这可能是一个很好的介绍: http://www.informit.com/guides/content.aspx?g=cplusplus&seqNum=386

【讨论】:

一个问题:如果一个included-header文件中的模板函数没有被使用,编译器是否足够聪明,不会编译它的内容并完全丢弃它? @curiousexplorer 是的——更进一步,模板可能永远不会在那种情况下被实例化。没有实例化就不会创建任何定义(例如,模板的特定用途,其中定义了模板参数)。程序需要被解析,但没有实例化就没有定义。

以上是关于关于 C/C++ 中使用的 header-guards 和 header-file 包含的说明的主要内容,如果未能解决你的问题,请参考以下文章

在 C/C++ 中使用 swift 库?

Linux C/C++关于结构体定义,typedef关键字的使用场景

C++/C 使用过程中的经验总结荟萃(持续更新)

C++/C 使用过程中的经验总结荟萃(持续更新)

C++/C 使用过程中的经验总结荟萃(持续更新)

C/C++关于函数调用传递实参