当仅包含.cpp文件时,一切正常时,为什么需要包含.h?


18

为什么我们需要同时包含.h.cpp文件,而仅通过包含.cpp文件就可以使其起作用?

例如:创建一个file.h包含声明,然后创建一个file.cpp包含定义,并将两者都包含在中main.cpp

或者:在中创建一个file.cpp包含声明/定义的声明(没有原型)main.cpp

两者都为我工作。我看不出有什么区别。也许对编译和链接过程有一些了解可能会有所帮助。


分享您的研究成果对每个人都有帮助。告诉我们您尝试过的内容以及为什么它不能满足您的需求。这表明您已花时间尝试自我帮助,这使我们免于重复显而易见的答案,并且最重要的是,它可以帮助您获得更具体和相关的答案。另请参阅“ 如何问”
t 2014年

我强烈建议您从侧面查看相关问题。程序员

为什么在程序员上而不是在SO上呢?
与莫妮卡(Monica)进行的轻度比赛

@LightnessRacesInOrbit,因为它是编码样式的问题,而不是“如何”的问题。
CashCow

@CashCow:听起来您误会了SO,这不是 “如何”网站。听起来您似乎误解了这个问题,这不是编码风格的问题。
与莫妮卡(Monica)进行的轻度比赛

Answers:


29

尽管您可以包括.cpp提到的文件,但这不是一个好主意。

如前所述,声明属于头文件。当它们包含在多个编译单元中时,这些不会引起任何问题,因为它们不包括实现。多次包含一个函数或类成员的定义通常会导致问题(但并非总是如此),因为链接器会感到困惑并抛出错误。

应该发生的是,每个.cpp文件都包含程序子集的定义,例如类,按逻辑组织的函数组,全局静态变量(请尽量使用)。

然后,每个编译单元.cpp文件)都包含编译其包含的定义所需的任何声明。它跟踪其引用但不包含的函数和类,因此链接器可以在稍后将目标代码组合到可执行文件或库中时解析它们。

  • Foo.h ->包含类Foo的声明(接口)。
  • Foo.cpp ->包含类Foo的定义(实现)。
  • Main.cpp->包含主要方法,程序入口点。此代码实例化Foo并使用它。

双方Foo.cppMain.cpp需要包括Foo.hFoo.cpp需要它是因为它正在定义支持类接口的代码,因此它需要知道该接口是什么。Main.cpp之所以需要它,是因为它正在创建Foo并调用其行为,因此它必须知道该行为是什么,Foo在内存中的大小以及如何找到其功能等,但它尚不需要实际的实现。

编译器将生成Foo.oFoo.cpp里面包含了所有的编译形式Foo类代码。它还生成Main.o包括main方法和对类Foo的未解析的引用。

现在到了连接器,它结合了两个目标文件Foo.o,并Main.o转换成可执行文件。它看到了未解析的Foo引用,Main.o但是看到了Foo.o包含必需的符号,因此可以说“连接点”。Main.o现在,将函数调用连接到已编译代码的实际位置,因此在运行时,程序可以跳转到正确的位置。

如果您将Foo.cpp文件包括在中Main.cpp,则将有Foo类的两个定义。链接器将看到此消息并说“我不知道该选哪个,所以这是一个错误。” 编译步骤将成功,但链接不会成功。(除非您不进行编译,Foo.cpp但是为什么要在单独的.cpp文件中进行编译?)

最后,不同文件类型的想法与C / C ++编译器无关。它编译“文本文件”,希望其中包含所需语言的有效代码。有时它可能能够基于文件扩展名分辨语言。例如,.c在没有编译器选项的情况下编译文件,它将假定为C,而a .cc.cpp扩展名将告诉它假定为C ++。但是,我可以很容易地告诉编译器将一个.h甚至一个.docx文件编译为C ++,.o如果它包含纯文本格式的有效C ++代码,它将发出一个object()文件。这些扩展更多地是为了程序员的利益。如果看到Foo.hFoo.cpp,则立即假定第一个包含类的声明,第二个包含定义。


我不明白您在这里提出的论点。如果你只是包括Foo.cppMain.cpp你不需要包含.h文件,你少了一个文件,你还是赢在分裂码成单独文件的可读性,和你的编译命令更加简单。虽然我了解头文件的重要性,但我不认为这是事实
Benjamin Gruenbaum 2014年

2
对于琐碎的程序,只需将它们全部放在一个文件中即可。甚至不包括任何项目标头(只是库标头)。包含源文件是一种不良习惯,一旦拥有多个编译单元并实际分别编译它们,就会对除最小程序以外的任何程序造成问题。

2
If you had included the Foo.cpp file in Main.cpp, there would be two definitions of class Foo.关于该问题的最重要的句子。
marczellm 2014年

@Snowman对,我认为您应该关注-多个编译单元。粘贴带有/不带有头文件的代码以将代码分解为更小的片段是有用的,并且与头文件的用途正交。如果您要做的只是将其拆分为文件头文件,那么我们什么都没买-只要我们需要动态链接,条件链接和更高级的构建,它们就会非常有用。
本杰明·格伦鲍姆

有很多方法来组织C / C ++编译器/链接器的源代码。询问者不确定该过程,因此我解释了如何组织代码以及该过程如何工作的最佳实践。您可以为可能的不同方式组织代码而烦恼,但是事实仍然存在,我解释了其中99%的项目是如何做到的,这是最佳实践,这是有原因的。它运作良好,并保持您的理智。

9

阅读来自角色C和C ++预处理器,其在概念上是C或C ++编译器的第一“阶段”(历史上它是一个单独的程序/lib/cpp;现在,出于性能原因是集成编译器内部的正常cc1cc1plus)。请特别阅读GNU cpp预处理程序的文档。因此,实际上,编译器在概念上首先会对您的编译单元(或翻译单元)进行预处理,然后对预处理后的表单进行处理。

如果包含标头文件,则可能需要始终包含标头文件file.h(由惯例习惯决定):

  • 宏定义
  • 类型定义(例如typedefstructclass等...)
  • static inline功能定义
  • 外部函数的声明。

注意将这些放在头文件中是惯例(和方便)的问题。

当然,您的实现file.cpp需要以上所有,因此#include "file.h" 首先要。

这是一个约定(但很常见)。您可以避免头文件,并将其内容复制并粘贴到实现文件(即翻译单元)中。但是您不希望这样做(除非自动生成C或C ++代码;否则,您可以使生成器程序进行该复制和粘贴,从而模仿预处理器的角色)。

关键是预处理器仅执行文本操作。您可以(原则上)完全通过复制和粘贴来避免使用它,或者用另一个“预处理器”或C代码生成器(例如gppm4)替换它。

另一个问题是最新的C(或C ++)标准定义了几个标准头文件。大多数实现实际上将这些标准标头实现为(特定于实现的)文件,但我相信,符合规范的实现可能会通过一些魔术(例如使用某些数据库或某些)来实现标准包含(如#include <stdio.h>C或#include <vector>C ++)。编译器内部的信息)。

如果使用GCC编译器(例如gccg++),则可以使用-H标志获取有关每个包含项的信息,并使用-C -E标志获取经过预处理的表单。当然,还有许多其他影响预处理的编译器标志(例如-I /some/dir/,添加/some/dir/用于搜索包含的文件,以及-D预定义一些预处理器宏,等等,等等。)。

注意 C ++的未来版本(也许是C ++ 20,甚至更高版本)可能具有C ++模块


1
如果链接腐烂,这仍然是一个很好的答案吗?
Dan Pichelman 2014年

4
我编辑了答案以改进它,并仔细选择了指向Wikipedia或GNU文档的链接,我相信它们会在很长一段时间内保持相关性。
Basile Starynkevitch 2014年

3

由于C ++的多单元构建模型,您需要一种使代码仅在程序中出现一次(定义)的方法,并且需要一种使代码在程序的每个翻译单元中出现的方法(声明)。

由此诞生了C ++标头习惯用法。这是有原因的约定。

可以将整个程序转储到单个翻译单元中,但这会带来代码重用,单元测试和模块间依赖项处理方面的问题。这也只是一团糟。


0

从所选择的答案为什么我们需要写一个头文件?是一个合理的解释,但我想补充其他细节。

头文件的合理性似乎在教学和讨论C / C ++时容易迷失方向。

头文件提供了两个应用程序开发问题的解决方案:

  1. 接口与实现的分离
  2. 大型程序的编译/链接时间缩短了

C / C ++可以从小型程序扩展到大型的数百万行,数千个文件程序。应用程序开发可以从一个开发人员的团队扩展到数百个开发人员。

作为开发人员,您可以戴几顶帽子。特别是,您可以是函数和类的接口的用户,或者可以是函数和类的接口的编写器。

使用函数时,您需要了解函数接口,要使用的参数,函数返回的内容以及函数的功能。无需查看实现即可轻松将其记录在头文件中。您什么时候读过的实现printf?购买我们每天都在使用。

当您是界面开发人员时,帽子会改变另一个方向。头文件提供了公共接口的声明。头文件定义了另一个实现需要什么才能使用此接口。头文件中不会(也不应)声明此新接口的内部和私有信息。公共头文件应该是使用该模块所需的所有文件。

对于大规模开发,编译和链接可能需要很长时间。从数分钟到数小时(甚至数天!)。将软件分为接口(头)和实现(源),提供了一种仅编译需要编译的文件而不是重新构建所有内容的方法。

此外,头文件允许开发人员提供库(已编译)以及头文件。库的其他用户可能永远看不到正确的实现,但是仍可以将库与头文件一起使用。您每天都使用C / C ++标准库执行此操作。

即使您正在开发小型应用程序,使用大型软件开发技术也是一个好习惯。但是我们还需要记住为什么要使用这些习惯。


0

您可能会问,为什么不将所有代码都放在一个文件中。

最简单的答案是代码维护。

在某些情况下,创建类是合理的:

  • 全部内联在标题中
  • 全部都在一个编译单元内,根本没有头文件。

完全内联到头文件中是合理的时候,该类实际上是一个具有几个基本getter和setters的数据结构,并且也许是一个采用值初始化其成员的构造函数。

(需要全部内联的模板是一个稍微不同的问题)。

在头中创建类的其他时间是您可能在多个项目中使用该类,并且特别希望避免在库中链接时。

您可能将整个类包含在编译单元中而根本不公开其标头的时候是:

  • 一个“ impl”类,仅由其实现的类使用。它是该类的实现细节,在外部不使用。

  • 由某种工厂方法创建的抽象基类的实现,该工厂方法返回指向基类的指针/引用/智能指针。工厂方法不会由类本身公开。(此外,如果类具有通过静态实例在表上注册自己的实例,则甚至不需要通过工厂公开它)。

  • “ functor”类型类。

换句话说,您不希望任何人包含标头。

我知道您可能在想...如果只是出于可维护性,通过包含cpp文件(或完全内联的标头),您可以轻松地编辑文件以“查找”代码并重新构建。

但是,“可维护性”不仅仅是使代码看起来整洁。这是变化影响的问题。众所周知,如果您不更改标头,而只是更改实现(.cpp)文件,则不需要重建另一个源,因为应该没有副作用。

这样就可以更安全地进行此类更改,而不必担心连锁效应,而这实际上就是“可维护性”的含义。


-1

Snowman的示例需要一些扩展名才能确切说明为什么需要.h文件。

将另一个班级Bar添加到剧本中,这也取决于班级Foo。

Foo.h->包含类Foo的声明

Foo.cpp->包含类Foo的定义(实现)

Main.cpp->使用类型为Foo的变量。

Bar.cpp->也使用Foo类型的变量。

现在,所有cpp文件都需要包含Foo.h。将Foo.cpp包含在多个其他cpp文件中将是一个错误。链接器将失败,因为将多次定义类Foo。


1
也不是。无论如何,这可以通过解决.h文件中的问题的完全相同的方法来解决-使用包含保护,并且与.h文件中的问题完全相同。这根本不是.h文件的全部。
本杰明·格伦鲍姆

我不确定您将如何使用include卫士,前提是它们只能针对每个文件工作(就像preprocessod一样)。在这种情况下,由于Main.cpp和Bar.cpp是不同的文件,因此Foo.cpp在两个文件中仅包含一次(无论是否受保护都没有任何区别)。Main.cpp和Bar.cpp都可以编译。但是由于类Foo的双重定义,仍然会发生链接错误。但是也有我没有提到的继承。宣言的外部模块(DLL的)等等
SkaarjFace

不,它将是一个编译单元,由于包含了.cpp文件,因此根本不会进行任何链接。
Benjamin Gruenbaum 2014年

我没有说它不会编译。但是它将不会在同一可执行文件中同时链接main.o和bar.o。不链接就根本没有意义。
SkaarjFace 2014年

嗯...让代码开始运行?您仍然可以链接它,但不能将文件彼此链接-而是它们将是同一编译单元。
Benjamin Gruenbaum 2014年

-2

如果将整个代码写在同一个文件中,那么它将使您的代码很难看。
其次,您将无法与他人分享您的书面课堂。根据软件工程,您应该分别编写客户端代码。客户端不应该知道您的程序如何工作。它们只需要输出。如果将整个程序写在同一个文件中,将会泄漏程序的安全性。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.