logo头像

Hacked By Swing

非专业计算机基础 (3)

接上篇

在之前的两篇中,我们基本上了解了计算机运行的原理了,也知道了程序大概是怎么运行起来的,为什么可以同时运行多个程序,还知道了操作系统是怎么样的。关于运行的部分我们基本上都已经了解过了。不过我们对 “制作” 还并不了解,也就是程序是什么我们已经知道了,但是程序是如何 “开发” 出来的呢?我们一般用的是 “写” 程序这么个名词,程序是怎么写出来的呢?这一篇我们就主要来解决这个问题。

汇编语言和机器语言

程序,其实也是数据,在之前我们介绍过程序需要装载到内存中才可以进行运行,而平时程序就以操作系统规定好的形式存储在硬盘上。运行的时候,操作系统会查看存储在硬盘上的程序文件,然后查看其中包含的信息,之后根据其中包含的信息,把文件中为具体的 “指令数据” 的部分装载到内存中,然后开始执行内存中这个程序的起始指令。

指令数据的部分我们在第一篇中已经提到过了,指令也是数据,数据具体是什么含义是由 CPU 自己规定的,这些指令数据我们称作机器代码,或者机器语言,比如我们可以规定数字 0 的含义为 “吃饭” 这条指令,数字 1 的含义为 “做饭” 这条指令,那么 0 和 1 就是机器语言,还可以继续规定数字 2 的含义,数字 3 的含义等等。

这样规定对于 CPU 来说很方便,但是对于编写程序的程序员来说就比较麻烦了,因为程序员必须记住每一个指令的含义,然而数字 0 和做饭这个事情没有任何关联,所以不方便记忆,为了方便记忆,聪明的程序员就想到了一个方法:利用一些简洁的助记符(用来表示指令的含义),操作数(用来表示指令操作在哪里,也就是第一篇中讲到的寄存器,操作在什么寄存器上)来编写程序。

这样编写出来的程序,程序员看起来就比较容易了,也比较容易编写,这种程序叫做汇编语言,比如一条真实的汇编语言指令如下:

1
add rax, rax ; rax 是寄存器的名字,这条指令的含义是将 rax 寄存器中的值和它自己的值相加(也就是乘 2 的操作),然后再存储到 rax 中

但是这样编写的文本,对于程序员来说虽然方便记忆了, CPU 又不认识了,CPU 只认识它所规定的数据,那么应该怎么处理呢?方法就是,我们先继续使用机器代码编写(也就是直接写指令数据)一个 “汇编器” ,由于 CPU 规定的指令和汇编代码基本是匹配的(也就是基本是一种一一对应的关系,一个数字对应一种汇编指令的形式),汇编器就可以直接将汇编代码的文本形式翻译为机器代码。

通过这种方法,程序员编写程序的时候就不再需要直接编写机器代码了,从此以后,程序员就可以通过编写汇编代码来得到指令数据了。

编程语言

虽然已经不必去记难以记忆的机器语言了,但是汇编语言依然不够方便,因为还是得去记 CPU 具体有哪些指令。另外,其实 CPU 与 CPU 之间也并不相同,一个 CPU 所有的指令和使用规则被称作 “指令集” ,每一种 CPU 的指令集是不一样的,比如电脑上使用的 CPU 的指令集和手机上使用的 CPU 的指令集就不一样,所以汇编语言也就不一样。那么如果想要写一个程序可以在不同种类的 CPU 上使用,现在的这种方法依然不够方便。

不过有了之前的经验,程序员也很容易地就想到了处理的方法:再发明一种语言,然后编写这个语言的一个 ”翻译器“ ,将这个语言翻译为汇编语言,汇编语言又可以翻译成机器语言,就可以得到最后的程序。而使用这种语言编写的程序就只需要写一次,我们只需要编写针对不同 CPU 的翻译器就可以了。

这就是后来的编程语言的来源,”翻译器“ 用术语应该被称作 ”编译器“ ,翻译过程也相应地被称作编译。

通过将这些编程语言设计得让程序员容易编写,程序员的工作量被进一步的降低了。

编译器和解释器

现在我们总结一下利用编译器和编程语言编写程序的一个过程:

  1. 使用编程语言编写程序
  2. 利用编译器将程序编译为汇编语言
  3. 利用汇编器将程序汇编为机器代码
  4. 机器代码也就是操作系统上的程序,可以被多次运行,之后的运行也就不再需要编程语言了

这样的工作方式本身是没有问题的,但是随着计算机的使用,计算机的使用者发现我们经常需要做重复的事情,比如我们会想要连续的执行多个程序,像是在更新完成后关机之类的,这些操作并不复杂,但是特点就是无法确定具体的使用场景,比如有时候我需要连续运行 A 和 B 程序,但是有时候又需要运行 B 和 C 程序,甚至连需要运行的程序的数量也不固定。

按照之前的使用方法,一种方式就是每次有这种需求的时候,都编写一个程序,然后使用编译器进行编译(其实现在的编译器一般都会将汇编器的过程也自己进行了,所以只需要进行编译就可以得到最终可以运行的程序了),这样的过程就会比较麻烦。

除了提到的这种情况以外,另一种情况就是,如果我们编写的程序希望在不同的机器上运行,而这些机器的 CPU 各不相同(也就是使用不同的指令集),那么按照现有的方法就是对不同的指令集分别进行一次编译,编译为不同的程序,然后在不同的机器上运行其对应的程序。

可以看到,在这些情况下,现在的这种编程的使用方式显得并不是特别方便,所以聪明的程序员就又想到了一种新的使用方法:编译器也是一个软件,其实编译器自己也可以执行编程语言中指定的任务。

这种直接对编程语言进行执行而不再编译出不同机器上各自需要的程序的 “编译器” 叫做 “解释器” ,其运行过程我们叫做 “解释运行” ,运行过程为:

  1. 使用编程语言编写程序
  2. 使用解释器运行编程语言(文本)

除此以外,解释器还可以进行 “在线” 运行,也就是用户一边输入编程语言一边运行,过程为:

  1. 用户输入编程语言的一部分指令
  2. 解释器运行这一部分指令
  3. 解释器等待用户输入,回到第一步

可以看到,这种运行方式相比之前的运行方式来说简单了不少,可是缺点也很明显,由于直接利用解释器运行程序,解释器本身需要被针对不同的机器进行编译才能够在不同的机器上运行,另外,另一个问题是由于在编译的过程中,编译器是可以看到完整的程序的,所以编译器可以进行综合地分析,然后再生成汇编代码,但是对于解释器来说,由于第二种运行方式的存在,解释器不知道用户输入的内容是否已经完整,所以只能用比较笨的方式直接运行,所以速度比较慢(当然这个速度其实还有别的原因造成,比如加法如果使用汇编语言可能只需要一条指令,但是解释运行的过程会经过大量的检查、处理等操作,每一次执行都需要,这样就将速度明显变慢了)。

JIT

现在我们知道了编译器和解释器了,也了解了它们的区别,下面我们来简单地说一种特殊的技巧,通过这样的技巧可以最大程度的结合编译器和解释器各自的优点。

这种技巧叫做 JIT ,全称为 Just-In-Time Compilation ,中文为及时编译。其实它的思路并不复杂,既然编译器可以让程序的执行速度更快,但是解释器又可以提高灵活性,那么我们何不结合它们各自的优点呢?也就是在解释执行的过程中,如果有一部分指令经常执行,那么就将它编译。

其实解释器和编译器的对比有一点类似一条俗语,叫做磨刀不误砍柴工,编译器是先磨刀,然后砍柴,而解释器是直接砍柴,JIT 就是先直接砍柴,如果砍着砍着发现斧头实在不好用,再磨一会刀,不用磨太久,然后再砍,等到觉得不好用的时候再磨。

一些编程语言的示例

下面我们将大家熟悉的编程语言简要地介绍一下:

  • C语言:编译型语言,需要编译运行,是比较早的编程语言
  • C++ :在兼容 C 语言的基础上再发展了一些编程语言的新思想,由于兼容 C 语言使得这个语言的复杂度一直比较高,特性很多(也就是支持的功能很多,使用方法很多样),使得学习难度大,不同的程序员写出来的风格差距比较大,也是它面临的难题
  • Python:解释型语言,通过解释的方法运行,速度慢,但是使用方便,在许多平台(也就是不同指令集)都可以使用,带有的库很多,比较方便日常使用(比如写一些临时的任务,批量做一些事情等等),也是目前机器学习的主流语言,近期由于机器学习等领域的发展,该语言发展情况不错。不过需要注意的是,Python 目前具有两个大的版本 2 和 3 ,不同大版本之间是不兼容的,版本 2 即将在 2020 年停止维护(也就是如果出现了问题不再修复了)
  • Java:JIT 语言,在大型项目上使用较多,语言风格比较冗长,写起来比较长,但是本身学习难度并不是很大,运行速度比 Python 快很多。
  • Javascript:与 Java 没有什么关系,这个名字主要来源于在 Java 盛行的时候蹭热点,真实原名应该为 ECMAScript ,JIT 语言,一般用在浏览器中,不过不同的浏览器中都有自己的实现方法(也就是不同的浏览器中运行的 JIT 程序并不相同),不过通过一个规范大家基本保持兼容(不过依然可能出现不兼容的情况,所以有时候不同网站需要要求你运用特定的浏览器运行),运行速度比 Python 快,但是总体来说比 Java 略慢,灵活性很强。

结语

本篇主要对编程语言的来源还有编译器和解释器进行了介绍,学习一门编程语言首先需要大致了解其背后的运行过程,否则使用起来碰到的问题就难以解决。