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

Posted

技术标签:

【中文标题】从 Java / C# 的角度理解 C++ 编译器【英文标题】:Understanding C++ compilers from a Java / C# perspective 【发布时间】:2009-02-10 16:33:22 【问题描述】:

我是一名经验丰富的 Java/C# 程序员,最近开始学习 C++。问题是,我无法理解如何构建各种头文件和代码文件。这似乎主要是由于我对编译器如何将所有内容链接在一起缺乏了解。我尝试阅读一些教科书,但我的 Java 和 C# 知识严重影响了我的先入之见。例如,我很难理解方法等可以在命名空间中定义而不仅仅是在类定义中的事实。

我找到了很多 C++ -> Java/C# 指南,但实际上没有其他方法。有没有什么好的资源可以缓解 Java/C# -> C++ 的转换,尤其是在理解编译过程方面?

【问题讨论】:

编辑:感谢所有回答的人,所有答案都很有用且内容丰富。 对于任何处于类似情况的人,我发现此页面非常有帮助,尤其是第 A3.3 节(类):horstmann.com/ccj2/ccjapp3.html 【参考方案1】:

C++ FAQ 是关于 C++ 的所有特性的极好资源,但它可能比您正在寻找的要高级一些——即使对于相当有经验的 C++ 来说,大多数问题(不仅仅是答案)都是谜开发人员。

我认为,如果您在谷歌上搜索 C++ 教程,您将能够找到一些东西。您可能还想尝试学习汇编语言(或者至少快速介绍一下微处理器中实际发生的事情),因为 C 和 C++ 在做事方式上都非常接近硬件。这就是它们的速度和力量的来源,但它是以 Java 提供的一些更好的抽象为代价的。

我可以尝试回答您上面提出的具体问题,但我不知道我会做得如何。

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

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

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

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

这最终比我预期的要长得多,当然是过于简单化了,但据我所知和所提供的详细程度,它是准确的......希望它对一些人有所帮助。至少它应该为您提供一些谷歌搜索的起点。

【讨论】:

【参考方案2】:

当我第一次开始使用 C 时,这也让我感到困惑。书籍并没有很好地描述标题与代码文件的正确使用。

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

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

【讨论】:

""头文件应该包含类的声明""事实上,你在头文件中定义类并声明它的成员函数。然而,定义那些成员函数/静态成员是在源文件中完成的。同时,类 foo;是一个声明,类 foo 是定义。【参考方案3】:

我实际上建议远离关于 C++ 编译器的解释,并查看 C 编译器的解释。以我的经验,这些都得到了更好的解释,避免让您对 OOP 问题感到困惑。寻找有关 C 单独编译的材料。我会向你推荐我母校的一本很棒的幻灯片小册子,但它不是英文的。

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

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

【讨论】:

【参考方案4】:

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

链接和编译是分开完成的,因为(并且可能有不止一个原因)需要进行库调用。如果您定义或任何类似的代码,则在这些头文件中实现函数原型的代码是已经编译并作为目标代码位于某处的库的一部分。如果要改用一个巨大的编译过程,您需要拥有这些库调用的源代码,以及在编译期间更多的时间,因为您还要编译库代码。

【讨论】:

以上是关于从 Java / C# 的角度理解 C++ 编译器的主要内容,如果未能解决你的问题,请参考以下文章

C#虚方法virtual详解

[C++] C++ 的常量究竟是什么? 它与 C# 和 Java 中的常量有什么区别? 应该如何理解常量?

新书速递深入理解Java虚拟机HotSpot

嵌入式单声道:将数组从 C# DLL 返回/复制到 C++

从 C++ 调用 C# dll(MSVC 编译器)

如何在编译时驱动 C#、C++ 或 Java 编译器计算 1+2+3+...+1000?