从Java/C#的角度,了解C++编译器

6
我是一名有一定经验的Java / C#程序员,最近开始学习C++。问题是,我不太理解如何组织各种头文件和代码文件。这似乎主要是由于我对编译器如何将所有东西链接在一起的理解不足。我尝试阅读了一些教材,但我的预设观念受到了我的Java和C#知识的影响。例如,我很难适应在命名空间中定义方法等事实,而不仅仅是在类定义中。
我发现有很多C++ -> Java/C#指南,但实际上没有什么东西可以用来进行反向转换。是否有任何好的资源可以帮助我更好地理解编译过程,特别是从Java/C#到C++的过渡?

编辑:感谢所有回答的人,所有的答案都很有用和信息丰富。 - Whatsit
对于处于类似情况的任何人,我发现这个页面非常有帮助,特别是A3.3节(类):http://www.horstmann.com/ccj2/ccjapp3.html - Whatsit
4个回答

4

当我开始使用C语言时,这也曾使我感到困惑。书籍并没有很好地描述头文件与代码文件的正确使用方法。

编译器通过加载每个.cpp文件并独立地对其进行编译来工作。编译的第一步是加载所有由#include语句引用的头文件。你可以把它看作是在任何出现#include "foo.h"的地方进行整个foo.h的文本插入。

这对于如何组织文件有什么影响呢?头文件应该包含其他.cpp文件需要引用的程序部分。一般规则是,实现不应该在头文件中。这会引起问题。头文件应该包括类、函数和全局变量(如果必须使用它们)的声明。


“头文件应包含类的声明”,实际上,在头文件中您需要定义类并声明其成员函数。然而,这些成员函数/静态成员的定义是在源文件中完成的。同时,class foo; 是一个声明,而 class foo{} 则是定义。 - Özgür

4

C++ FAQ 是一个关于C++的特异性的优秀资源,但它可能比你想要的更高级——大部分问题(不仅仅是答案)即使对于经验丰富的C++开发人员也是谜。

我认为如果你搜索C++教程,就能找到一些东西。你也可以尝试学习汇编语言(或至少了解微处理器实际操作的快速介绍),因为C和C++都非常接近硬件。这是它们速度和功率的来源,但这是以Java提供的一些更好的抽象为代价的。

我可以尝试回答你上面提出的具体问题,但我不知道我能做得有多好。

理解头文件和cpp文件之间关系的关键之一是理解“翻译单元”的概念。Java类文件可以被视为一个翻译单元,因为它是编译成二进制形式的基本单元。在C++中,几乎每个cpp文件都是一个翻译单元(如果你做奇怪的事情,则存在例外情况)。

头文件可以包含在多个翻译单元中(并且必须在使用头文件定义的任何地方都包含它)。 #include指令只是进行文本替换——被包含文件的内容会原封不动地插入到#include指令的位置。通常希望类接口在头文件中定义,实现在cpp文件中。这是因为您不希望将实现细节暴露给可能包含头文件的其他翻译单元。在C++中,所有内容(包括类)实际上并不是丰富的对象,而只是编译器赋予意义的一块内存……通过将相同的头信息编译到每个翻译单元中,编译器保证所有翻译单元对内存块表示的理解都是一样的。由于缺乏丰富的数据,如反射等事物是不可能的。

C++构建过程的第二个步骤是链接,其中链接器获取所有已编译的翻译单元,并查找其中一个翻译单元中未定义的符号(通常是函数调用,但也包括变量)。然后查找另一个定义该符号的翻译单元,并将它们“链接”在一起,以便将对特定函数的所有调用都指向定义它的翻译单元。

在类方法的情况下,它们必须通过类实例调用,这在幕后只是指向一块内存的指针。当编译器看到这些类型的方法调用时,它会输出调用函数的代码,隐式地将指针(称为this指针)作为第一个参数传递给函数。你可以有不属于类的函数(不是方法,因为方法是正确的类成员函数,因此不能没有类而存在),因为链接器没有类的概念。它将看到定义函数的翻译单元和调用函数的翻译单元,并将它们联系在一起。

这篇文章比我预想的长很多,当然也是一种过度简化,但据我所知和提供的细节水平来看还是准确的...希望对你有所帮助。至少它应该为你的谷歌搜索提供了一个起点。


1

我实际上建议远离关于C++编译器的解释,而是看看关于C编译器的解释。根据我的经验,这些解释更好,并避免了OOP问题的混淆。寻找有关C分离编译的材料。我本来想向您推荐我母校的一本精美幻灯片手册,但它不是用英语编写的。

C编译和Java/C#之间的主要区别在于编译不会创建已解决的实体。换句话说,在Java中编译时,编译器会查找任何引用类的已编译类文件,并确保所有内容都可用且一致。基本假设是当您最终运行程序时,这些文件也将可用。

编译后的C文件只是一个“承诺”。它依赖于声明的依赖关系(以函数声明的形式),但不能保证这些依赖关系在任何地方都被定义。你需要做的最困难的心态转变是,不仅把C文件看作是那个文件,而是将其与所有包含的内容(即预处理器生成的内容)聚合起来。换句话说,编译器看不到头文件,它似乎是一个大文件。编译器在生成的目标文件中跟踪所有“仍然缺失”的内容。稍后,在链接时,链接器通过尝试使用来自不同目标文件的材料来填补所有空白来解决这个问题。

1

你可能想知道为什么编译和链接是分开的(因为我没有看到任何解释它的帖子,而不知道事情的根本原因会造成很多困惑)。

链接和编译是分开完成的,因为(可能有多个原因)需要进行库调用。如果你定义或者类似的函数,实现这些头文件中函数原型的代码是已经编译好并作为目标代码存储在某处的库的一部分。如果使用巨大的编译过程,则需要源代码来进行这些库调用,并且需要更长时间进行编译,因为还需要编译库代码。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接