发布于 2026-01-06 1 阅读
0

编程语言的构建过程 DEV 全球展示与讲述挑战赛,由 Mux 呈现:展示你的项目!

编程语言的构建过程

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

本文是“基础课程中学不到的基础知识”系列文章的一部分,旨在帮助渴望深入了解编程和计算机科学的人们。

当我编写第一个程序时,我的导师告诉我,我写的只是源代码。现在,我必须把它翻译成计算机可以理解的语言。这叫做编译。在 Visual Studio 中,只需按下 Ctrl+F5 即可完成编译。这样,你的程序就变成了可执行文件。

他们最初就是这样教我理解程序构建过程的。对于初学者来说,这已经足够解释了。但后来我意识到,当我按下 Ctrl+F5 时,后台会发生一些我们看不到的进程。这些进程我们将在今天的文章中探讨。

你知道吗?按下 Ctrl+F5 时,不同编程语言的处理方式是不同的。你有没有想过为什么用 C++ 编程比用 C# 编程更难?

嗯,我们无法深入探讨最后一个问题的全部细节。这很大程度上与语言设计以及多年来做出的决策有关。但我们会探讨这些语言之间的根本区别。这种区别在于它们的构建过程。

编译成本地代码的语言

这类语言最流行的例子是 C 和 C++。

编译成本地代码是什么意思?

当我们编写程序时,代码会经历多个阶段,最终被转换成机器代码。机器代码是处理器直接执行的代码。更多相关内容,请参阅 本系列之前的文章。

以上是概览。但让我们先来深入了解一下将源代码转换为机器代码的内部过程。

为此,我们将探索使用 C 和 C++ 语言编写程序的过程,因为 C 和 C++ 是使用这种方法最流行的语言。

整个过程包括 4 个步骤——预处理、编译、汇编、链接

预处理

在 C 和 C++ 程序中,有一些称为预处理器指令的行,它们以# 开头。

预处理器使用这些指令来操作程序文本。例如,有一个`#include`指令。当预处理器遇到该指令时,它会提取指令指定的文件内容,并将其替换到首次遇到该指令的行。我们使用这种方法是为了重用代码,而不是在不同的文件中复制粘贴。

还有其他指令,但本文暂不探讨。一般来说,预处理器会将初始源代码转换为应用了所有文本转换的预处理代码。当然,结果会存储在一个临时文件中,不会覆盖原始文件。该文件将在编译步骤中进一步使用。

编译与汇编

在此步骤中,预处理后的代码将进一步转换为汇编代码。

接下来,另一个程序就派上了用场,它叫做汇编器。它的作用是将汇编代码转换成机器代码。(什么是汇编语言?它是如何工作的?请再查阅一下。

有些编译器可以直接将源代码翻译成机器代码,因此有可能跳过汇编步骤。但即便如此,也意味着编译器内部集成了汇编器。所以,汇编步骤可能只是隐式执行,但它仍然存在。

生成的机器代码会被打包成一个中间文件,称为目标文件。这些文件包含了来自单个源文件的所有数据。但正如您所知,一个程序中可能包含多个源文件。这意味着,仅靠一个目标文件不足以完成整个程序。这就需要最后一步,即链接。

链接

现在你的项目中已经有了十几个目标文件。这些文件是程序的各个独立模块。最后的链接阶段是将这些模块组合成一个单独的可执行文件。同时,在这个阶段,文件之间的各种依赖关系也会被解析。

例如,如果文件 A 调用了文件 B 中的一个函数,那么文件 A 的目标文件中会有调用该函数的指令。但是,该函数本身的指令并不存在于同一个目标文件中。链接器负责检查该函数是否实际存在于程序的任何其他目标文件中。在本例中,目标文件是文件 B。

基于虚拟机的语言

在这个级别中最流行的语言是 Java 和 C#。

这些语言不会直接编译成特定硬件的本地代码。相反,它们会编译成一种中间语言,然后由虚拟机执行。

虚拟机就像一台运行在你电脑上的模拟计算机。在我们的例子中,它的作用是执行生成的中间代码。它的执行方式是:首先识别运行的硬件类型,然后根据该信息将代码编译成机器的本地代码。

这个过程的理念是,只需为抽象的虚拟机构建一次程序,虚拟机就负责了解底层实际运行的硬件是什么。这样,就能实现平台无关性。

我们看到,在构建编译成本地代码(特别是 C 和 C++)的程序时,我们会经历几个过程。

但在这些语言中,整个构建过程仅限于将源文件编译成中间语言文件。将这些文件转换为计算机本地代码的过程则由虚拟机完成。

口译语言

常见的例子有 JavaScript、Python、Ruby 等。

这些语言完全没有编译过程。用这些语言编写的程序需要通过一个单独的程序——解释器——来执行。解释器逐行执行程序中的指令。相比之下,编译型语言的源代码会被作为一个整体进行分析和执行,而解释型语言则是逐行执行的。

这意味着你可以在执行一半程序后才遇到错误,而其他语言中,一个错误就足以阻止整个程序的执行(当然,前提是它是编译错误)。

每种语言都可以编译/解释执行吗?

在本文的初稿中,有人巧妙地指出,一些解释型语言也可以被编译,反之亦然。

所以,回答这个问题——是的,所有可解释的语言都可以被编译,所有可编译的语言也都可以被编译。要理解这一点,我们首先需要更深入地了解编译和解释究竟是什么。

编译意味着我想将语言 X 翻译成语言 Y,如果运行,它将给出与语言 X 相同的结果,但速度更快。

解释执行是指将用语言 X 编写的程序按照该语言定义的规则执行。说白了,就是运行这个程序。

所以本质上,编译和解释都意味着对语言进行转换。正因如此,任何可以编译的语言也都可以解释。

然而,有些语言被称为编译型语言,而另一些语言被称为解释型语言,是因为这些语言传统上是与编译过程一起使用的,并且它们的规则针对编译过程进行了优化。

例如,C++程序的编译是一个缓慢的过程,编译后的性能可以得到显著提升。但如果采用解释执行,同样的规则反而会导致程序运行速度变慢。

另一方面,用解释型语言编写的程序虽然规则使其易于快速解释执行,但如果改为编译执行,性能提升并不明显。例如,将 JavaScript 程序转换为其他语言几乎不会带来任何性能提升。

当然,上述说法也有例外,因为有各种现代技巧可以让你获得编译的一些好处(如 JIT 编译),但就其传统形式而言,这些语言并不适合编译。

那么,为什么选择其中一个而不是另一个呢?

我们已经了解了构建过程中有哪些不同类型的语言。但现在,让我们探讨一下在选择合适的语言来完成我们的工作时,它们之间有什么区别。

易于编码

首先,一个重要的方面是哪种语言更容易上手。而用 C 或 C++ 编程无疑是最难的。我认为,如果你学会了 C 和 C++ 编程,你就能学会任何其他语言。原因在于,它们最接近计算机的本质。正因如此,它们才更贴近我们人类的本性。

另一方面,用 Java 或 C# 编写代码比用 C 或 C++ 编写代码更容易,因为它们会被编译成中间语言,这种中间语言的抽象级别比本地代码更高。此外,虚拟机的辅助功能还能提供更好的错误处理、垃圾回收和其他特性,帮助我们编写更安全的代码。

解释型语言的抽象层次更高,使你能够用更少的代码实现更多功能。

可移植性

计算机科学领域的一大关注点是程序能否在多个平台上运行。对于编译成本地代码的语言来说,情况并非如此,因为生成的代码是针对特定硬件的。如果你想让你的 C 程序同时在 Windows 和 Linux 上运行,你必须针对不同的平台重新编译两次。如果程序使用了平台特定的指令,那么维护起来会更加困难。

另一方面,Java 和 C# 中虚拟机的作用是了解底层硬件,并将代码进一步编译成特定于平台的机器代码。用 Python 或 JavaScript 编写的程序也可以在任何机器上运行,只要机器上有支持这些语言的解释器即可。

表现

这正是编译成本地代码的程序的优势所在。它们无需启动庞大的虚拟机或运行几兆字节的解释器,就能直接运行。

然而,仅仅用 C 语言编写程序并不意味着它就一定比基于虚拟机的语言更快。Java 开发人员可以利用虚拟机在运行时优化代码。而用 C 语言编写程序时,优化程序是开发人员的责任。当然,编译器会帮你完成大部分工作,但在某些情况下,这可能还不够。

例如,如果您编译的 Java 程序运行在四核处理器上,虚拟机可能会检测到这一点并相应地优化您的程序。当然,前提是您创建的是多线程程序。例如,如果您将多个函数标记为异步执行,JVM 可以根据您使用的四核处理器,确定最佳线程数。

在 C 语言中,你必须自己完成前期工作,编写使用四核处理器的具体指令。

所以归根结底,像 C 和 C++ 这样的底层编程语言赋予你优化程序的自由,但优化仍然是你的工作。

记忆

C 和 C++ 在这方面也表现出色。不过,运行虚拟机或解释器时,内存方面会有额外的开销。

对于服务器来说,这可能不是问题,但对于嵌入式设备来说,却是一个重要的限制。例如,如果您想用 Python 对带有 8 KB 外部存储器的微控制器进行编程,则首先需要在设备上嵌入 2 MB 的 Python 解释器。

至于虚拟机类型的语言,虽然有一些版本更加简洁,专为嵌入式环境设计,但编译成本地代码的语言在内存使用方面仍然胜过它们。

结论

现在您了解了我们编写的程序的构建过程的基本原理。

总而言之,从构建过程来看,语言可以分为三种类型:编译成本地代码的语言;编译成中间语言代码或虚拟机代码的语言;以及解释执行的语言。

此外,通过这个简要概述,我们了解了为什么有些语言比其他语言运行速度更快,以及为什么我们会根据不同的目的选择不同的语言。

下次,我们将再次深入程序内部,探索什么是栈和堆,以及我们的程序如何使用它们。

文章来源:https://dev.to/pmihaylov/the-build-process-of-programming-languages-1i08