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

ial*_*alm 132 c++ header-files

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

回顾一下我的代码,我选择不为我的类创建头文件,但是在cpp文件中做了所有事情(它看起来没有头文件......).我猜这个评分者意味着我写了'#include'mycppfile.cpp";' 在我的一些文件中.

#include对cpp文件的推理是: - 应该进入头文件的所有东西都在我的cpp文件中,所以我假装它就像一个头文件 - 在猴子看猴子做时尚,我看到了其他头文件#include在文件中是'd,所以我为我的cpp文件做了同样的事情.

那究竟是我做错了什么,为什么不好呢?

gol*_*udo 160

据我所知,C++标准在头文件和源文件之间没有区别.就语言而言,任何带有合法代码的文本文件都与其他任何文本文件相同.但是,虽然不是非法的,但是将源文件包含到程序中几乎可以消除您从一开始就分离源文件所带来的任何好处.

本质上,#include告诉预处理器获取您指定的整个文件,并在编译器获得它之前将其复制到活动文件中.因此,当您将项目中的所有源文件包含在一起时,您所做的事情和创建一个没有任何分离的巨大源文件之间基本上没有区别.

"哦,这没什么大不了的.如果它运行,那很好,"我听到你哭了.从某种意义上说,你是对的.但是现在你正在处理一个微小的小程序,以及一个很好且相对无阻碍的CPU来为你编译它.你不会总是这么幸运.

如果您曾经深入研究严肃的计算机编程领域,您将会看到项目的行数可以达到数百万而不是数十.那是很多台词.如果您尝试在现代台式计算机上编译其中一个,则可能需要几个小时而不是几秒钟.

"哦不!这听起来很可怕!但是我可以防止这种可怕的命运吗?!" 不幸的是,你无能为力.如果编译需要数小时,则编译需要数小时.但这只是第一次真正重要 - 一旦你编译了一次,就没有理由再次编译它.

除非你改变了什么.

现在,如果你有两百万行代码合并成一个庞大的庞然大物,并且需要做一个简单的错误修复,比如说x = y + 1,这意味着你必须再次编译所有两百万行来测试它.如果你发现你打算做一个x = y - 1代替,那么再过两百万行编译等着你.这浪费了很多时间,可以更好地做其他事情.

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

"但肯定一定是可能的!编程听起来像是纯粹的折磨!如果我找到一些方法将接口与实现分开怎么办?假​​设从这些不同的代码段中获取足够的信息以将其识别到程序的其余部分,并放置相反,它们在某种文件中?这样,我可以使用#include 预处理器指令只引入编译所需的信息!"

嗯.你可能会在那里做点什么.让我知道这对你有什么影响.

  • 好的答案,先生.这是一个有趣的阅读,易于理解.我希望我的教科书写得像这样. (12认同)
  • 这是(明确的)迄今为止我听到或考虑过的最佳措辞.杰出的初学者贾斯汀凯斯(Justin Case)获得了一个项目时钟,其中有一百万次击键,尚未发货,一个值得称道的"第一个项目"正在真正的用户群中看到应用之光,已经认识到闭包解决了一个问题.听起来非常类似于OP的原始问题定义的高级状态减去"编码这近百倍,并且无法在不使用异常编程的情况下为null(作为无对象)vs null(作为nephew)做什么." (2认同)
  • 另一点是,您有许多最先进的库(如果想到 BOOST),它们仅使用标头类... 嗬,等等?为什么有经验的程序员不将接口与实现分开?部分答案可能是 Blindly 所说的,另一部分可能是,在可能的情况下,一个文件比两个文件更好,另一部分是链接的成本可能相当高。我已经看到,通过直接包含源代码和编译器优化,程序的运行速度提高了十倍。因为链接主要会阻碍优化。 (2认同)

小智 43

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

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

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

/* Function declaration, usually found in headers. */
/* Implicitly 'extern', i.e the symbol is visible everywhere, not just locally.*/
int add(int, int);

/* function body, or function definition. */
int add(int a, int b) 
{
   return a + b;
}
Run Code Online (Sandbox Code Playgroud)

如果希望函数对于翻译单元是本地的,则将其定义为"静态".这是什么意思?这意味着如果您包含具有extern函数的源文件,您将获得重定义错误,因为编译器不止一次地遇到相同的实现.因此,您希望所有翻译单元都能看到函数原型而不是函数体.

那么这一切最终如何被捣碎在一起呢?这是链接器的工作.链接器读取汇编程序阶段生成的所有目标文件并解析符号.正如我之前所说,符号只是一个名称.例如,变量或函数的名称.当调用函数或声明类型的转换单元不知道这些函数或类型的实现时,这些符号被认为是未解析的.链接器通过连接保存未定义符号的转换单元和包含该实现的转换单元来解析未解析的符号.唷.对于所有外部可见符号都是如此,无论它们是在代码中实现还是由其他库提供.库实际上只是一个包含可重用代码的存档.

有两个值得注意的例外.首先,如果你有一个小功能,你可以使它内联.这意味着生成的机器代码不会生成外部函数调用,而是按字面顺序连接.由于它们通常很小,所以尺寸开销并不重要.你可以想象它们在工作方式上是静态的.因此,在头文件中实现内联函数是安全的.类或结构定义中的函数实现通常也由编译器自动内联.

另一个例外是模板.由于编译器在实例化时需要查看整个模板类型定义,因此无法将实现与定义分离,就像独立函数或普通类一样.嗯,也许现在可能这样,但是对"export"关键字的广泛编译器支持需要很长时间.因此,如果不支持"导出",翻译单元将获得自己的实例化模板化类型和函数的本地副本,类似于内联函数的工作方式.由于支持"出口",情况并非如此.

对于这两个例外,有些人发现将内联函数,模板化函数和模板化类型的实现放在.cpp文件中"更好",然后#include .cpp文件.这是标头还是源文件并不重要; 预处理器不关心,只是一个约定.

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

  • 预处理器运行时,其解析与一个"#"开始的所有指令.例如,#include指令将包含的文件与劣质文件连接起来.它还进行宏替换和令牌粘贴.
  • 实际编译器在预处理器阶段之后在中间文本文件上运行,并发出汇编代码.
  • 汇编器在组件文件运行并发出机器代码,这通常被称为一个目标文件和如下所讨论的手术系统的二进制可执行的格式.例如,Windows使用PE(可移植可执行格式),而Linux使用带有GNU扩展的Unix System V ELF格式.在此阶段,符号仍标记为未定义.
  • 最后,运行链接器.所有前面的阶段都按顺序在每个翻译单元上运行.但是,链接器阶段适用于汇编程序生成的所有生成的目标文件.链接器解析符号并做很多魔术,比如创建段和段,这取决于目标平台和二进制格式.程序员一般不需要知道这一点,但在某些情况下肯定会有所帮助.

同样,这比你要求的要多得多,但我希望这些细节可以帮助你看到更大的图景.

  • +1.有些问题没有简短的答案. (5认同)
  • 谢谢你的详尽解释.我承认,这对我来说并没有任何意义,我想我需要再次阅读你的答案(并再次). (2认同)

sha*_*oth 9

典型的解决方案是.h仅使用文件进行声明,使用.cpp文件进行实现.如果需要重用实现,则将相应的.h文件包含在.cpp必要的类/函数/使用的文件中,并链接到已编译的.cpp文件(.obj文件 - 通常在一个项目中使用 - 或.lib文件 - 通常使用从多个项目中重用).这样,如果仅实现更改,则无需重新编译所有内容.


Dan*_*ath 6

将cpp文件视为黑盒子,将.h文件视为如何使用这些黑盒子的指南.

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

虽然这对你的第一个项目没有多大影响,但如果你开始编写大型cpp程序,人们就会讨厌你,因为编译时间会爆炸.

另请阅读:头文件包含模式


int*_*nt3 6

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

这种分离的好处:如果要重新编译项目中的一个.cpp文件,则不必重新编译所有其他文件.您只需为该特定.cpp文件创建新的目标文件.编译器不必查看其他.cpp文件.但是,如果要调用当前.cpp文件中的函数,这些函数是在其他.cpp文件中实现的,则必须告诉编译器它们采用了哪些参数; 这是包含头文件的目的.

缺点:编译给定的.cpp文件时,编译器无法"看到"其他.cpp文件中的内容.因此,它不知道如何实现这些功能,因此无法积极地进行优化.但我认为你现在还不需要关心它(:


Luk*_*ský 5

仅包含标头的基本思想和仅编译cpp文件.一旦你有很多cpp文件,这将变得更有用,并且当你只修改其中一个文件时重新编译整个应用程序将会太慢.或者当文件中的函数相互依赖时.因此,您应该将类​​声明分离到头文件中,将实现保留在cpp文件中并编写Makefile(或其他东西,具体取决于您使用的工具)来编译cpp文件并将生成的目标文件链接到程序中.