我到处搜索,无法弄清楚。我重读了维基百科,总的来说重读了所有内容。我真的不明白。
是什么阻止了只包含 .cpp 文件?
嗯,你连接了两次,好吧,让编译器只连接一次就这样了,因为它把所有东西都粘在一个文件中,这意味着第一个连接将高于其他连接,并且对下面的它们可见。让它查看更改时间并仅重新编译更改的内容。让它自动生成带有接口描述的头文件,附加到编译后的二进制文件等。
换句话说,为什么生成头文件的常规工作没有自动化并委托给开发人员呢?毕竟,除了已编译的二进制文件之外,编译器本身可以轻松生成接口描述文件(它通过扫描 cpp 文件接收到)。
你能想到不使用文件头会出现问题的情况吗?这将是最好的解释。
问题出在向后兼容方面。
看,任何新的编程语言——甚至是 Pascal,更不用说 Java 或 C#——都不需要头文件。毫无疑问,C++ 也可以没有它们。怎么了?
快进半个世纪到 1972 年。想象一个 C 编译器。
假设我们要编写一个编译器设计。我们不能一次编译整个程序,我们根本没有足够的内存。那时的计算机又小又慢。我们想一段一段地编译程序,一次编译几个函数。
我们立即遇到一个问题:如何编译
f引用另一个函数的函数g?我们需要单独描述其他功能。我们当然可以把所有的源文件都读一遍,先找出自己有什么功能,然后再读一遍,一个一个编译。但是太复杂太慢了,需要解析两次函数定义,一次抛出结果!这是对 CPU 时间的不可接受的浪费!另外,如果您将所有函数的定义保存在内存中,同样,内存可能不足。Dennis决定把函数的声明与其实现分开并在编译该函数时只包括必要的声明这一难题交给谁?对于我们程序员。他决定我们应该自己帮助编译器并将函数定义复制粘贴到一个单独的文件中,并告诉编译器我们自己需要哪些包含定义的文件。(也就是说,编译的第一步是我们的责任。)
这从根本上简化了编译器,但反过来又导致了问题。如果我们忘记包含所需的头文件会怎样?答:编译错误。如果头文件文本的含义根据某些宏而改变,会发生什么情况?答:编译器很“笨”,并没有尝试检测这个问题,它把责任推给了我们。
在开发语言时,这是正确的决定。事实证明,编译器实用、快速,并且程序员愿意帮助编译。好吧,如果有人犯了错误,他自己就是罪魁祸首。
将时钟的指针倒回到 1983 年。Bjarne创建了 C++。他决定乘着C语言流行的浪潮,直接从C中采用独立翻译单元和相关问题的C编译模型。然而,C++的第一个版本只是C语言的预处理器!因此,单独编译的问题从C迁移到了C++。更糟糕的是,还增加了新的问题。例如,类模板看起来像类,但不会自己生成目标代码,因此它们必须去技巧并绕过单独编译系统的缺点(例如,包括在头文件中的实现和链接器技巧) .
然后向后兼容性开始发挥作用。现在,在 2017 年,有这么多代码以“with headers”风格编写,还有这么多代码来自与之相关的各种微妙之处,以至于现在改变范式已经来不及了,火车几乎已经开走了。
然而,有一个用 C++ 编写模块化系统的项目,应该可以帮助程序员摆脱半个世纪前的遗留问题。还没有实现,有设计层面的复杂性(比如header里定义了一个宏,从header转到modules会不会可见?)希望以后语言开发者还是会能够克服向后兼容性。
程序的汇编发生在三个阶段:预处理器、编译器和链接器。
处理指令的预处理器会将内容
#include "file"替换到当前文件中。file原来,如果你在几个编译文件中包含一个cpp文件,那么它会被编译多次。这就是单一定义规则发挥作用的地方。
同时阅读什么是Translation unit。
我举个例子:
A类.cpp
B类.cpp
C类.cpp
执行预处理器后,你的ClassB.cpp和ClassC.cpp会分别变成
和
直到现在才开始编译。即使你不单独编译ClassA.cpp,你仍然违反单一定义规则(ClassA定义在两个翻译单元中,ClassB.cpp和ClassC.cpp)。
历史上就是这样发生的。使用自动生成的头文件需要一个复杂的构建系统,该系统可以在模块之间存在复杂依赖关系的情况下确定编译顺序。
而且由于在语言形成时还没有发明这样的系统,所以它们没有将头文件的生成添加到编译器中。而且到目前为止,这样的系统还没有出现,因为编译器不知道如何生成头文件。
也许新标准中的模块会改变一切。
在任何情况下,对于导入 .cpp 文件不适用的情况,头文件将保留。例如,当没有.cpp 文件时——连接第三方库时。或者如果模块之间存在循环依赖。或者只是在对头文件施加额外要求的情况下,使其自动生成不可接受。
预计在C++17中会有第一个标准化版本的模块,即支持没有头文件的程序集。据我了解,在这个版本中,模块将作为编译器扩展实现,也就是说,这个功能在C++ 17中是可选的。
但是,模块已经在Visual Studio 2015 Update 1和clang中实现,因此您可以试用它们。
缺乏有关可从外部获得的类型的信息。
编译器把源代码文件(
.c和.cpp)变成目标文件(.obj后来被链接器链接成单个.exe或.dll文件)-“半成品”-“黑盒子”,导入和导出符号。反过来,符号是“函数/变量名称 - 它相对于目标文件开头的偏移量”的组合。导入是目标文件的依赖项(它们在何处实现并不重要,只需要名称匹配);导出是在这个文件中实现的。
因此,目标文件不包含仅存在于编译器想象中的数据类型和其他抽象。只有在 RAM 中以实际分配空间和命名标签的形式存在的机器代码和数据。
不幸的是,仅仅知道名称还不足以形成导入(依赖)。编译器还需要知道参与类型的结构,以便生成正确的堆栈偏移量、插入额外的构造函数/析构函数调用、执行隐式类型转换、验证调用等。
由于这个信息不在目标文件中(我提醒你,只有名称),所以必须在这个外部实体要使用的源代码文件中以声明的形式描述。描述可以在
.cpp文件本身中执行,也可以在单独的文件中取出.hpp并包含在需要类型描述的任何地方。为什么类型的描述没有内置到实现它们的目标文件中?相同的数据类型可以在任意数量的地方使用,因此这样的内联违反了单一定义规则。根据这个规则,每个实体应该只有一个源(目标文件),这样链接器在比较导入和导出时就不会有不确定性。
用C和C++构建程序分两个阶段进行。
编译器将每个源代码文件 (
.c和.cpp) 转换为目标文件(.obj)。同时,每个源代码文件都是独立编译的,仿佛宇宙中只有它一个,除此之外再无其他存在。在这个阶段,编译器看不到其他目标文件,但它已经有义务产生现成的机器代码。语言开发人员选择这种方法来实现编译的并行化。链接器获取指定的目标文件(无论它们是如何或何时获得的),将它们的导入和导出链接在一起,并将它们组合成一个可执行文件(
.exe或.dll)。这是
.cpp给您的文件(不包括您想要的非标准标头):编译器将如何从中推断出类结构
Logger?即:哪些字段应该在类中,它们的访问修饰符是什么?毕竟,一个类的实现可以分布在多个文件中,就像一个文件可以包含多个类的部分方法的实现一样。您可能会说:“
class Logger{...};在这里描述,以便从中生成它.hpp”。那么,如果我们将此描述放入头文件中,有什么区别呢?我的类.cpp
主.cpp
多一个文件.cpp
等等每个文件使用
Foo.现在想象一下,对类定义的每次更改都需要在很多地方进行大量替换。如果您忘记修复它,编译器将保持沉默,被误导,最终程序将崩溃。
如果我不想要一个怎么办?
假设我由于宏而生成了一组相似的函数,其中一些最小的行为差异是由其他宏决定的。那。我没有生成代码,而是更改常量的值并将相同的 cpp 文件包含在另一个 cpp 文件中。你想剥夺我这个机会吗?
这是一个示例:http: //ideone.com/TlN4r0。尽管由于您必须将所有内容都塞进一个文件(为了 ideone)这一事实而导致不必要的复杂化,但这个想法应该很清楚。此外,假设函数足够大,但差异足够小。在这种情况下,将整个函数放在一个宏中就变得不合理了。