为什么我不应该包含cpp文件,而是使用头?

所以我完成了我的第一个C ++编程任务,并获得了我的成绩。 但根据评分,我没有including cpp files instead of compiling and linking them标记, including cpp files instead of compiling and linking them 。 我不太清楚这意味着什么。

回头看看我的代码,我select不为我的类创build头文件,但在cpp文件中做了所有事情(似乎无需头文件就能正常工作…)。 我猜测,分级意味着我写了“#include”mycppfile.cpp“;” 在我的一些文件中。

我对#include的cpp文件的理由是: – 所有应该进入头文件的东西都放在我的cpp文件中,所以我假装它就像一个头文件 – 在猴子看到猴子做时尚的时候,我看到其他的头文件是#include在文件中,所以我做了同样的我的CPP文件。

那么我究竟做错了什么,为什么这么做呢?

就我所知,C ++标准知道头文件和源文件没有区别。 就语言而言,任何带有合法代码的文本文件都是相同的。 然而,尽pipe不是非法的,包括源文件在内的程序几乎可以消除将源文件分离出来的任何优势。

从本质上讲, #include作用是告诉预处理器把你指定的整个文件拷贝到你的活动文件中,然后编译器到达它的位置。 所以当你把所有的源文件一起包含在你的项目中时,你所做的事情和根本没有分离的一个巨大的源文件之间基本上没有区别。

“哦,没什么大不了的,如果跑的话,没关系,”我听到你哭了。 从某种意义上说,你是对的。 但是现在你正在处理一个微小的小程序,以及一个不错的相对不占用CPU的CPU来编译它。 你不会总是那么幸运。

如果您深入了解严肃的计算机编程领域,您将看到线数可达数百万而不是数十的项目。 这是很多线路。 如果您尝试在现代桌面计算机上编译其中的一个,则可能需要几小时而不是几秒钟的时间。

“哦,不,这听起来很可怕,但是我能阻止这种可怕的命运吗? 不幸的是,你可以做的事情不多。 如果编译需要几个小时,编译需要几个小时。 但这只是第一次真正重要 – 一旦你编译一次,没有理由再编译一次。

除非你改变一些东西。

现在,如果你有两百万行代码合并成一个巨型巨兽,并且需要做一个简单的错误修正,比如x = y + 1 ,那就意味着你必须再次编译所有二百万行代码testing这个。 如果你发现你打算做一个x = y - 1 ,那么再有200万行编译正在等着你。 这浪费了很多时间,可以做更好的事情。

“但是我讨厌没有生产力!如果只有某种方法可以单独编译我的代码库的不同部分,然后以某种方式它们连接在一起!” 理论上是一个很好的想法。 但是如果你的程序需要知道在不同的文件中发生了什么呢? 除非你想运行一堆微小的.exe文件,否则完全分离你的代码库是不可能的。

“但是一定是可能的!编程听起来像是纯粹的折磨!否则,如果我find了一种将接口与实现分离的方法呢?比如说,从这些不同的代码段中获取足够的信息来识别它们到程序的其余部分,他们在某种文件?而且这样,我可以使用#include 预处理器指令来引入只有编译必要的信息!

嗯。 你可能会在那里。 让我知道这是如何解决你的。

这可能是比你想要的更详细的答案,但我认为一个体面的解释是合理的。

在C和C ++中,一个源文件被定义为一个翻译单元 。 按照惯例,头文件包含函数声明,types定义和类定义。 实际的函数实现驻留在翻译单元中,即.cpp文件。

这个背后的想法是函数和类/结构成员函数被编译和汇编一次,然后其他函数可以从一个地方调用该代码而不重复。 你的函数原型被隐式声明为“extern”。

 /* Function prototype, usually found in headers. */ /* Implicitly 'extern', ie the symbols is visible everywhere, not just locally.*/ int add(int, int); /* function body, or function definition. */ int add(int a, int b) { return a + b; } 

如果你想要一个函数作为翻译单元的本地,你可以将它定义为“静态”。 这是什么意思? 这意味着如果包含带有extern函数的源文件,将会得到重定义错误,因为编译器不止一次地出现在相同的实现中。 所以,你希望所有的翻译单位都能看到函数原型而不是函数体

那么最后怎么样呢? 这是链接器的工作。 链接器读取由汇编器阶段生成的所有目标文件并parsing符号。 正如我刚才所说,一个符号只是一个名字。 例如,variables或函数的名称。 当调用函数或声明types的翻译单元不知道这些函数或types的实现时,这些符号被认为是未解决的。 链接器通过将保存未定义符号的翻译单元与包含实现的翻译单元连接在一起来parsing未parsing的符号。 唷。 所有外部可见的符号都是如此,无论它们是在您的代码中实现还是由附加的库提供。 一个库实际上只是一个可重用代码的存档。

有两个明显的例外。 首先,如果你有一个小function,你可以把它内联。 这意味着生成的机器代码不会生成外部函数调用,而是直接串联在一起。 由于它们通常很小,所以大小的开销并不重要。 你可以想象它们在工作方式上是静态的。 所以在头文件中实现内联函数是安全的。 类或结构体定义中的函数实现也经常由编译器自动内联。

另一个例外是模板。 由于编译器在实例化时需要看到整个模板types定义,因此不可能像独立函数或普通类一样将实现从定义中分离出来。 那么,现在也许这是可能的,但是获得广泛的编译器支持“导出”关键字花了很长时间。 因此,如果不支持“导出”,翻译单元会获得自己的实例化模板types和函数的本地副本,类似于内联函数的工作方式。 支持“出口”,情况并非如此。

对于这两个例外,有些人认为将内联函数,模板化函数和模板化types的实现放在.cpp文件中是“更好的”,然后#include .cpp文件。 不pipe这是头文件还是源文件都不重要, 预处理器不关心,只是一个约定。

从C ++代码(几个文件)到最终可执行文件的整个过程的快速总结:

  • 运行预处理器 ,parsing所有以“#”开头的指令。 例如,#include指令将包含的文件与低级连接在一起。 它也做macrosreplace和标记粘贴。
  • 实际的编译器在预处理器阶段之后在中间文本文件上运行,并发出汇编代码。
  • 汇编程序在汇编文件上运行并发出机器代码,通常称为目标文件,并遵循所讨论的操作系统的二进制可执行格式。 例如,Windows使用PE(便携式可执行格式),而Linux使用Unix System V ELF格式,并使用GNU扩展。 在这个阶段,符号仍被标记为未定义。
  • 最后, 链接器运行。 之前的所有阶段都按顺序在每个翻译单元上运行。 但是,链接器阶段对由汇编器生成的所有生成的目标文件起作用。 链接器parsing了符号,并且像创build节和段一样执行了很多魔术,这取决于目标平台和二进制格式。 程序员通常不需要知道这一点,但在某些情况下肯定有帮助。

再一次,这比你所要求的要多得多,但我希望细节的细节可以帮助你看到更大的局面。

典型的解决scheme是仅使用.h文件进行声明,并使用.cpp文件进行实现。 如果您需要重新使用该实现,则将相应的.h文件包括到必需的类/函数/所用的.cpp文件中,并与已编译的.cpp文件(一个.obj文件 – 通常在一个项目中使用 – 或.lib文件 – 通常用于从多个项目中重用)。 这样,如果只有实现更改,则不需要重新编译所有内容。

把cpp文件想象成一个黑盒子,把.h文件当作关于如何使用这些黑盒子的指南。

cpp文件可以提前编译。 这不适用于你#include他们,因为每次编译时都需要将代码“包含”到你的程序中。 如果只包含头文件,则可以使用头文件来确定如何使用预编译的cpp文件。

虽然这对于你的第一个项目来说不会有很大的不同,但如果你开始编写大型的cpp程序,人们会讨厌你,因为编译时间会爆炸。

也有这样的读取: 头文件包含模式

头文件通常包含函数/类的声明,而.cpp文件包含实际的实现。 在编译时,每个.cpp文件被编译成一个目标文件(通常是扩展名.o),链接器将各种目标文件组合成最终的可执行文件。 链接过程通常比编译要快得多。

这种分离的好处:如果你正在重新编译项目中的一个.cpp文件,你不必重新编译所有其他文件。 您只需为该特定的.cpp文件创build新的对象文件。 编译器不必看其他.cpp文件。 但是,如果你想调用在其他.cpp文件中实现的当前.cpp文件中的函数,则必须告诉编译器他们所用的参数; 这是包含头文件的目的。

缺点:编译一个给定的.cpp文件时,编译器不能“查看”其他.cpp文件中的内容。 所以它不知道那里的function是如何实现的,结果是不能很好地进行优化。 但是我认为你不需要关心那些(:

标题只包含和cpp文件的基本思想只编译。 一旦你有很多的cpp文件,这将变得更有用,而当你修改其中的一个时,重新编译整个应用程序将会太慢。 或者当文件中的function将根据彼此开始。 所以,你应该把类声明分离到你的头文件中,在cpp文件中保留实现,并编写一个Makefile(或者其他的东西,这取决于你使用的是什么工具)来编译cpp文件,并将生成的目标文件链接到一个程序中。

如果你在你的程序中的#include一个cpp文件在其他几个文件中,编译器会尝试多次编译cpp文件,并且会产生一个错误,因为会有多个相同方法的实现。

如果在#included cpp文件中进行编辑,编译将花费更长的时间(这将成为大型项目的问题),然后强制重新编译任何文件(包括它们)。

只要把你的声明放到头文件中,并包含这些声明(因为它们本身并不实际生成代码),链接器将声明与相应的cpp代码(然后只被编译一次)挂钩。

虽然可以像你一样做,但标准做法是将共享声明放入头文件(.h),函数和variables的定义(执行)到源文件(.cpp)中。

作为一个惯例,这有助于明确所有内容,并明确区分模块的接口和实现。 这也意味着你不需要检查一个.cpp文件是否被包含在另一个文件中,然后添加一些东西,如果它被定义在几个不同的单元中,可能会中断。

重用性,体系结构和数据封装

这里是一个例子:

假设你创build一个cpp文件,其中包含一个简单forms的string例程,在一个类mystring中,你把这个类的decl放在一个mystring.h中编译mystring.cpp到一个.obj文件

现在在你的主程序(例如main.cpp)中包含标题和链接mystring.obj。 在你的程序中使用mystring你不关心mystring是如何实现的,因为头文件说明它可以做什么

现在,如果一个好友想要使用你的mystring类,你给他mystring.h和mystring.obj,他也不一定需要知道它是如何工作,只要它工作。

以后如果你有更多这样的.obj文件,你可以将它们合并成一个.lib文件并链接到。

您也可以决定更改mystring.cpp文件并更有效地实施它,这不会影响您的main.cpp或您的好友程序。

如果它对你有用,那么它就没有什么不对,只不过它会扰乱那些认为只有一种办法做事的人的羽毛。

这里给出的许多答案都针对大型软件项目进行优化。 这些都是很好的事情,但是把一个小项目作为一个大项目来优化是没有意义的 – 这就是所谓的“不成熟优化”。 根据您的开发环境,设置构buildconfiguration以支持每个程序的多个源文件可能会有额外的复杂性。

如果随着时间的推移,您的项目不断发展,并且您发现构build过程耗时过长, 那么您可以重构代码以使用多个源文件来实现更快的增量构build。

几个答案讨论分离界面与实施。 然而,这不是包含文件的固有特性,并且直接包含它们的实现(甚至C ++标准库在很大程度上这样做)的#include“头文件”是很常见的。

唯一真正“非常规”的做法就是命名您的包含文件“.cpp”而不是“.h”或“.hpp”。

编译和链接程序时,编译器首先编译单个cpp文件,然后链接(连接)它们。 头文件永远不会被编译,除非首先包含在一个cpp文件中。

通常标头是声明,cpp是实现文件。 在头文件中,你为类或者函数定义了一个接口,但是你忽略了你实际实现细节的方式。 这样你就不必重新编译每一个cpp文件,如果你在一个更改。

我会build议你通过John Lakos的大型C ++软件devise 。 在大学里,我们通常写一些小的项目,我们不会遇到这样的问题。 本书突出了分离接口和实现的重要性。

头文件通常具有不被频繁改变的接口。 类似的,像Virtual Constructor成语这样的模式将帮助你进一步把握这个概念。

我还在学习像你:)

这就像写一本书,你只想打印出完成的章节一次

假设你正在写一本书。 如果将章节放在单独的文件中,那么只需要修改它就可以打印出一章。 编写一章不会改变任何其他章节。

但从编译器的angular度来看,包括cpp文件在内,就像在一个文件中编辑本书的所有章节一样。 然后,如果你改变它,你必须打印整本书的所有页面,以便打印修改后的章节。 目标代码生成中没有“打印选定的页面”选项。

回到软件:我有Linux和Ruby src四处闲逛。 粗略的测量代码行

  Linux Ruby 100,000 100,000 core functionality (just kernel/*, ruby top level dir) 10,000,000 200,000 everything 

这四类中的任何一个都有很多代码,因此需要模块化。 这种代码基础是现实世界系统的典型代表。