「程序」是怎样炼成的

「程序」是怎样炼成的 #

🚧本章仍在施工中,内容并不完整且会发生变化,仅供先行阅读参考。

本章含有较宽的表格、公式、代码块等内容,建议您在更宽的屏幕上阅读,以获得最佳体验。

近些年,仿佛有一股「少儿编程」「全民编程」之风席卷了中国:在大街小巷上,「编程辅导」的广告层出不穷;在中小学校里,程序设计的知识走进了课堂。即使你已经步入职场,类似「学编程知识,提升『生产力』」之类的培训广告仍然随处可见。在这股浪潮之下,我们有必要先回答一个更基本的问题——「程序」是什么?看完这一章,你将可以找到这些问题的答案:

  • 什么是「程序」?「编程」是要做什么?
  • 什么是 Python、C 语言、C++、Java……
  • 各种各样的程序,是如何在电脑的硬件上运行的?
  • 什么是「32 位」「64 位」?什么是「x86」,什么是「ARM」?

在开始之前,我们希望强调的是:《你缺计课》不是一本编程书。在本章,我们不会深入地介绍任何编程语言,更不会教你代码怎么写。相反地,我们会帮助你理解「编程在干什么」,以及「程序如何运行」。

早在《你缺计课》的第一章 认识你的电脑 我们就介绍过,电脑是由「硬件」和「软件」有机组成的整体。事实上,平板电脑、手机、智能手表——我们身边的一切智能设备,都由硬件和软件组成。这其中,「硬件」对应着电路、芯片以及各种外部设备,而软件就对应着各种各样的计算机程序,简称程序

我们在电脑上使用的 Word 和浏览器、在手机上聊着的微信和 QQ、在平板上看着的各种视频 app,它们就是一个个程序。而 Windows、iOS、安卓这些操作系统,它们亦是一类特殊的程序。人们把「编写程序」的过程简称「编程」,这是今天许多人的工作。也许你对这项工作充满了好奇:我们今天使用的各种各样的程序,是如何被设计出来的呢?又或许你会疑惑:这些千变万化的程序,是怎么在冷冰冰的硬件上运行的呢?本章就让我们揭开「程序」这个熟悉又陌生之物的面纱,带你了解它背后的秘密。

程序?菜谱! #

化繁为简的程序思维 #

让我们先来看一个生活中的「做菜」情景。「番茄炒蛋」是一道经典的家常菜,我们可以将烹饪它的过程归纳为这样的 5 个步骤:

  • 准备材料:打蛋、洗净番茄切块;
  • 单独炒鸡蛋:将鸡蛋炒至基本成型但保持嫩滑的状态,待用;
  • 单独炒番茄:将番茄炒至变软,略开始出汁的状态;
  • 合并一起炒,调味:将之前炒好的鸡蛋倒入,翻炒均匀,加入调味料;
  • 出锅。

对于会做菜的读者,即使你从来没有炒过番茄炒蛋,看过这份菜谱之后,你也应该可以大差不差地将它做出来。然而,如果你是一位没有炒菜经验,甚至从来没有下过厨房的读者,这份步骤就略显笼统了。在这种情况下,我们可以先约定几个最简单、最基本的「操作」,如「切」「洗」「翻炒」等。这些操作十分机械化,可以在很短的时间内掌握它们。借助这些基础操作,我们的番茄炒蛋菜谱就可以改写为:

  • 准备材料:
    • 番茄 3 个 → 得到「洗净的番茄」;
    • 洗净的番茄 → 得到「番茄块」;
    • 鸡蛋 4 个 → 得到「鸡蛋液」;
  • 单独炒鸡蛋:
    • 点火
    • 向锅中加入 20 mL;
    • 等待 1 分钟;
    • 向锅中加入 鸡蛋液
    • 翻炒 2 分钟;
    • 熄火
    • 从锅中取出 炒好的鸡蛋 → 得到「炒好的鸡蛋」;
  • 单独炒番茄:
    • 点火
    • 向锅中加入 10 mL;
    • 等待 40 秒;
    • 向锅中加入 番茄块
    • 翻炒 4 分钟;
  • 合并一起炒:
    • 向锅中加入 炒好的鸡蛋
    • 翻炒 1 分钟;
    • 向锅中加入 2 g;
    • 翻炒 1 分钟;
    • 熄火
  • 出锅:
    • 从锅中取出 番茄炒蛋 → 得到「番茄炒蛋」。
  • 准备材料:
    • 番茄 3 个 → 得到「洗净的番茄」;
    • 洗净的番茄 → 得到「番茄块」;
    • 鸡蛋 4 个 → 得到「鸡蛋液」;
  • 单独炒鸡蛋:
    • 点火
    • 向锅中加入 20 mL;
    • 等待 1 分钟;
    • 向锅中加入 鸡蛋液
    • 翻炒 2 分钟;
    • 熄火
    • 从锅中取出 炒好的鸡蛋 → 得到「炒好的鸡蛋」;
  • 单独炒番茄:
    • 点火
    • 向锅中加入 10 mL;
    • 等待 40 秒;
    • 向锅中加入 番茄块
    • 翻炒 4 分钟;
  • 合并一起炒:
    • 向锅中加入 炒好的鸡蛋
    • 翻炒 1 分钟;
    • 向锅中加入 2 g;
    • 翻炒 1 分钟;
    • 熄火
  • 出锅:
    • 从锅中取出 番茄炒蛋 → 得到「番茄炒蛋」。

在这份新的详细版菜谱中,只包含 10 种基本操作,并且对每一步的操作,都用定量的方式来确定它们。同时,我们给整个做菜过程的「中间产物」都起了名字,例如「均匀的鸡蛋液」「番茄块」,这样可以最大程度地避免歧义。理论上,借助这样的一份新菜谱,即使是完全没有烹饪经验的人,也可以在简单培训后,照着它完成任务。

我们把这种通过组合利用少量、基础的操作,完成一项庞大、复杂的任务的思维,叫做「程序思维」。在计算机的世界里,硬件本身只能「理解」少量最基本的操作,如最基本的算术运算,以及「读取文件」「显示内容」等简单的对外交互。程序就像一份菜谱:通过组合运用一些基本操作,实现各种各样的功能;而编程的过程,就是我们利用程序思维来编写这样的「菜谱」的过程。而这份番茄炒蛋菜谱,本质上则是一份程序的「源代码」,只不过处理它的设备并非计算机,而是我们人类。

顺序、分支与循环结构 #

在这个番茄炒蛋的菜谱中,我们将所有操作完全固定——从食材的量到翻炒的时间,都以具体的数字进行了规定。这样就降低了「硬件」的执行成本。然而,在现实生活中,起锅烧油所需要的时间可能与油的品质、气温、油量的多少等因素有关,而翻炒的时间也和食材的新鲜程度、切片时的厚度等挂钩。这种「固定一切」的思路,让整个程序过于死板,对环境没有适应能力。为此,我们再引入一种这样的操作:

  • 如果 番茄块 还没有变软:
    • 翻炒 30 秒;
  • 否则
    • 什么也不做。

如果说原来的程序是「做完一件事,就顺着做下一件事」的「一刀流」,那么在引入这种「如果」操作后,我们的程序长出了「分支」。在番茄比较生、气温比较低,或者水放少了的情况下,程序会进入「再翻炒 30 秒」这一支;而当番茄质地偏软、天气炎热,又或者油的品种不同时,程序就会选择什么也不做,然后继续向后运行。我们把原来的程序结构称为「顺序结构」,而这种引入「如果」的结构称为「分支结构」。

可是,分支结构只能实现「二选一」,通过嵌套多个分支也只能实现「三选一」「四选一」,程序能走出的道路终究是有限的。上面的例子让我们得以给不太好吃的番茄多炒 30 秒,可是如果 30 秒还是不够呢?进一步,在极端的情况下,再多 1 分钟也可能不够;而另一些时候,原本设定的 4 分钟翻炒可能已经多了一把「火」。分支结构在这样的情况下,「心有余而力不足」。为了解决这个问题,我们只能再采用一种新的操作:

  • 番茄块 还没有变软 时:
    • 翻炒 30 秒。

乍一看,这个操作和刚刚提到的「如果」挺相似,但是它们的含义完全不同。这个操作是说,只要「番茄块还没有变软」,就一直重复不断地执行「翻炒 30 秒」这一步。只有某时刻,「番茄块还没有变软」不满足了,才能离开这个「轮回」,继续执行后面的流程。这样的结构称为「循环结构」,顾名思义,它的作用就是在一定的条件下,不断重复地做同一件事,直到这个条件不再成立。相较之下,「分支结构」也需要检查某个条件,但是即使条件满足,也只会选择某个分支执行一次。

分支结构和循环结构有明显的不同,但是,只要再引入一种操作,就能让分支结构「变」为循环结构。这个操作是什么呢?答案就是「跳转到第 x 步」,其中 x 是某步操作的编号。例如,下面的程序没有使用循环结构的「当」操作,却实现了和上面的程序完全一样的效果。

  1. 如果 番茄块 已经变软:
  • 跳转到 第 4 步;
  1. 翻炒 30 秒;
  2. 跳转到 第 1 步;
  3. 什么也不做。

事实上,我们可以将原本固定的 4 分钟翻炒直接替换为上面的循环。这样,我们的程序就会从一开始就根据番茄是否变软来控制翻炒的时间,实现了「具体问题具体分析」。进一步,我们还可以把循环体中的「30 秒」缩得更短,这样程序控制的精度就越高。

顺序、分支和循环结构是程序的三种基本控制结构——事实上,只需要这三种结构,世间一切复杂之事,就都可以用程序思维分解为基础操作的组合。你不妨现在思考一下身边各种各样的事:小到日常生活中的鸡毛蒜皮,大到社会运行的底层原理,一切都可以归约成这三种结构组合之下的基本操作。这就是程序思维的力量,也是今天的数字世界能够诞生的重要基石。

编程语言 #

在上一小节中,我们设计了一个菜谱「程序」,并用中文写出了它的「源代码」:对于这个程序的每一步,我们都使用完整的中文句子来描述它的含义。然而,世界上的语言有成千上万种。我们大可以使用不同的语言来描述它——只要保持每一步的含义不变,那么整个程序的本质和功能就没有改变。例如,如果我们用英语来描述这个程序的第二段,除了得到的源代码「长得」不一样之外,它们在功能上是相同的:

  • Scramble Eggs:
    • Light the stove;
    • Add 40 mL of oil to the pot;
    • Wait for 40 seconds;
    • Scramble for 3 minutes;
    • Turn off the stove;
    • Set scrambled eggs aside.

诸如中文、英语、法语这样的语言,是我们人与人之间沟通的「桥梁」,而借助这样的桥梁,我们才得以用菜谱来指导他人完成烹饪。而在真正的计算机程序的世界里,人们亦设计了无数种不同的「编程语言」。我们可以利用不同的编程语言,为相同的功能写出不同的源代码。例如,为了计算

\[\sum_{i=1}^{100} i^2=1^2+2^2+\cdots+99^2+100^2\text{,}\]

我们可以用程序思维,设计出下面的计算流程:

  • sum 为 0;
  • index 为 1;
  • index 小于或等于 100 时:
    • sum 加上 index 的平方;
    • index 加上 1;
  • sum 就是最终的答案。

接着,我们使用几门编程语言「C 语言」「C++」「Java」「Python」「Rust」和「文言」,来将这个思路写成代码:

#include <stdio.h>
int main() {
    int sum = 0;
    for (int i = 1; i <= 100; ++i) {
        sum += i * i;
    }
    printf("答案是 %d", sum);
    return 0;
}
#include <iostream>
int main() {
    int sum = 0;
    for (int i = 1; i <= 100; ++i) {
        sum += i * i;
    }
    std::cout << "答案是 " << sum << std::endl;
    return 0;
}
public class SumOfSquares {
    public static void main(String[] args) {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i * i;
        }
        System.out.println("答案是 " + sum);
    }
}
print("答案是", sum(i**2 for i in range(1, 101)))
fn main() {
    let sum = (1..=100).map(|i| i * i).sum::<i32>();
    println!("答案是 {}", sum);
}

「文言」(wenyan-lang)还真是一门编程语言哦,你可以在它的官方网站查看更多信息。

吾有二數。曰一。曰零。名之曰「計」。曰「和」。
為是百遍。
  乘「計」以「計」。加其以「和」。昔之「和」者。今其是矣。
  加「計」以一。昔之「計」者。今其是矣。
云云。

吾有二言。曰「「答曰」」。曰「和」。書之。

纵览这几份使用不同编程语言写成的源代码,会发现它们风格各有不同:C 语言与 C++ 长得几乎一样,和 Java 亦有几分相似。同时仔细一看,会发现它们其实与我们用人类语言进行的思路描述结构一致。Python 和 Rust 则有更短的代码长度,Python 甚至在一行内就解决了问题。不过,要想看出这两份代码和我们思路之间的关系,就需要多花一些时间了。而至于那颇有我国古代数学著作风格的「文言」编程语言,它则和 C/C++ 一样,更加直观地刻画着我们的思路。这提醒着我们:不同的编程语言有着自己的「性格」,亦有着自己的适用范围。

除了「文言」之外,上面提到的几门编程语言都在当下比较常用。我们在这儿简要地介绍一下它们。

  • C 语言和 C++ 事实上是「父与子」的关系,C++ 是扩展功能之后的 C 语言,因此人们常用「C/C++」这样的名字来并称它们。它们是相当基础的编程语言,下限较低、上限很高,因此用途非常广泛。大多数操作系统,包括我们正在使用的 Windows,在底层都使用 C/C++ 开发。同时,大量的电脑软件、游戏引擎、乃至《你缺计课》的服务端软件1都是使用 C/C++ 编写的。
  • Java 相比与 C++,具有更清晰的语法,编写难度较低,既容易学习又功能丰富,同时它还是曾经开发安卓 app 的首选(可以说是唯一)语言。至今,仍然有大量的安卓 app 中残留了许多 Java 代码。同时,很多服务端软件、电脑游戏《我的世界》都使用 Java 开发。
  • Python 是一门极为灵活的语言——对于同样的任务,它可以用多种方式实现,能写出非常简洁的代码,并同时保持一定的可读性。Python 还是一门非常「开放」的语言:它拥有一个庞大的社区,其中有许多他人写好的可以直接供我们使用的「库」。这些「库」让 Python 几乎「无所不能」。不过,相比于 C/C++ 和 Java,Python 的性能非常差。这决定了 Python 主要被用在数据分析、科学研究、人工智能等领域,而不是用来编写「真正干活的」软件。也是基于类似的原因,Python 成了现在「编程入门」界的标配。
  • Rust 是一门新兴的编程语言,诞生至今不过十余年。它吸收了来自多种不同编程语言的优秀设计,并以「安全」作为一大特色——这能在一定程度上减少程序中漏洞的产生。目前,Rust 正在快速地发展,并已经在产业界开始应用——Linux 操作系统就正在开始使用 Rust 编写一部分代码。

世间有数不尽的编程语言,每年都会有无数新的语言诞生,各自标榜自己具有这样那样的优势。事实上,学习一门编程语言并不是难事——相反,最难的事情终究是「如何运用程序思维将问题分解」。下面,我们将简要地介绍「算法」,来更深刻地体会思维的重要性。

用好算法,事半功倍 #

在中小学学习数学的过程中,老师都喜欢强调一个概念「一题多解」。通常,一道题不止有一种解法;但是同时,这不同的解法之间,往往在复杂程度和用时方面存在差异。例如,解决立体几何问题,就有「建立坐标系」的解析法和直接计算法。在程序设计的世界里,解决相同的问题(实现相同的功能),亦有着不同的方式。我们把一套定义好的,可以由计算机按指示执行的步骤称为一种「算法」(algorithm)。显然,对于同一个问题,往往有许多种不同的算法,它们之间存在着差异。

假设现在我们要设计一个「程序」来炒出 100 盘番茄炒蛋。一个最简单的想法,便是重复执行上文中的菜谱程序 100 遍。这样做当然可行。不过,由于做每一盘都需要重复多次起锅烧油的过程,同时考虑实际场景的话,在「炒鸡蛋」和「炒番茄」两步之间来回切换亦会十分消耗时间。这样做的效率无疑是相当低下的。为了缓解这个情况,我们可以选择最大化利用我们的锅:假如我们的锅一次最多能炒 10 盘菜的量,我们就可以先一次性炒好 10 份的鸡蛋,再一次性炒出 10 盘菜来。这样,重复操作的数量就从原来的 100 次下降为 10 次——换算成时间,这是一笔不小的开销。

根据前文菜谱中给出的时间数值,计算采用这种方式能节省的时间。

这个例子虽然简单,但是它向我们强调了「好算法」的重要性:在问题固定的情况下,选择好的算法能节省更多的时间;在相同的时间内,选择好的算法又能解决更多的问题。我们再来分析一个问题:如果要计算

\[\begin{aligned} \sum_{i=1}^{100}i!&=1!+2!+\cdots+99!+100! \\ &=1+1\times 2+1\times 2\times 3+\cdots+1\times 2\times 3\times\cdots\times 100\text{,} \end{aligned}\]

我们当然可以逐个计算每一项阶乘的值: \(1!\) \(2!\) \(3!\) ……一直到 \(99!\) \(100!\) ,然后把它们相加。用这种方式,我们一共要进行这么多次计算:

  • 乘法: \(1!\) 无需使用, \(2!\) 需要 1 次, \(3!\) 需要 2 次…… \(100!\) 需要 99 次。一共是 \(\sum_{i=0}^{99}i=4950\) 次;
  • 加法:一共是 99 次。

而当我们考虑到 \(2!=2\times 1!\) \(3!=3\times 2!\) …… \(100!=100\times 99!\) 时,一个新的思路就呼之欲出了。由于每一项阶乘其实都是上一项阶乘再多乘一个数,我们可以把计算流程变成:

  • 先计算 \(1!=1\)
  • 再计算 \(2!=2\times 1!=2\)
  • 再计算 \(3!=3\times 2!=6\)
  • 再计算 \(4!=4\times 3!=24\)
  • ……
  • 再计算 \(100!=100\times 99!=93326215\cdots0000\)

最后将这 100 个结果相加即可。这个过程中,每一步都基于上一步的结果计算,只进行一次乘法,因此总共进行了 99 次乘法和 99 次加法。与之前相比,我们将运算量减少到了原来的 4%!如果原来的计算需要 100 毫秒,那么现在的计算就只需要 4 毫秒。对于许多问题,一个好的算法能让我们「事半半半,功倍倍倍」。

在计算机理论领域,人们针对大量的经典问题研究出了许多优秀的算法,如排序问题、搜索问题、图论问题等。在各大编程语言中,那些经典的优秀算法往往已经被实现成可以直接使用的「方法」,让人们在编程时可以直接「站在巨人的肩膀上」。

0 与 1 之间的舞蹈 #

至此,我们已经明白,通过各种各样的编程语言,借助程序思维,人们得以将需要完成的工作编写为程序;而通过对算法的研究,又能在极大程度上提升程序的效率,从而更有效地解决问题。然而,我们所认识的「程序」,与冷冰冰的芯片、导线、电路所构成的计算机硬件之间,依然还有着一层厚厚的「隔阂」。程序所奏响的思维之歌,是如何化为 0 与 1 之间的舞蹈的?下面,就由我们为你一一讲述。

程序、指令、机器代码 #

万言皆数——字符与编码规则 一章中我们提到,计算机中的一切信息都由电路承载;具体地说,每个信息都是由若干位 0 和 1 这样的二进制数字所组成,反映在电路上,就是「有电」「无电」之间的排列组合。为了实现程序在硬件上运行,自然而然地,我们首先就需要寻找「程序」在硬件上的表达方式。具体来说,是将程序由源代码转换为 0 和 1 组成的序列的方式。

在本章的第一节,我们构想了一个「番茄炒蛋」的程序,不过那时我们认为这个程序的「硬件」就是我们人类。现在,我们想要让一台炒菜机器「你缺饭课一号」也能执行这份菜谱程序。「你缺饭课一号」并不能看懂中文、英文这样的人类语言,因此不能直接看懂那份「源代码」。它所能做的,就是不断地读取某处并排的 8 根导线上的信号(即 8 位二进制数),然后根据这组信号的值,执行一个简单的动作。假设「你缺饭课一号」的部分设计如下:

  • 机器一共有 4 个用来装食材的碗,可以在烹饪过程中使用。
  • 机器不断地读取一个 8 位的信号,并按照下面的规则选择动作:
    信号 功能
    0001xxyy 洗一份食材。xx 决定食材的种类:00 代表番茄、01 代表青椒……
    yy 表示洗完之后的食材加在哪个碗里,一共有 0011 四个碗可以使用。
    例如,信号 00010000 能控制机器洗一份番茄放到第 0 号碗里。
    001000yy 打鸡蛋。yy 决定打好的蛋放在哪个碗里。
    001100yy 切菜。将 yy 号碗中的食材全部切块。
    01000xxx 给灶台点火,并向锅中加入的油。其中,油的量为 \(10\times\overline{\text{xxx}}_\text{B}\) mL 。例如,01000100 对应的 xxx100,这组信号会让机器点火后加入 \(10\times4=40\) mL 的油。
    010010yy 向锅中倒入碗 yy 中的全部内容。
    1000xxxx 在锅中翻炒 \(10\times\overline{\mathrm{xxxx}}_\mathrm{B}\) 秒。
    1100xxxx 等待 \(10\times\overline{\mathrm{xxxx}}_\mathrm{B}\) 秒。
    1110xxxx 向锅中加入 xxxx g 的盐。例如,11100101 对应的 xxx101,这组信号会让机器向锅中加入 5 g 盐。
    111100yy 将锅中的所有内容倒到 yy 号碗中。
    11111111 熄灭灶台。

那么,我们可以将源代码转换为下面这 24 条信号。依次给这台机器发送这些信号,机器就能按照菜谱精准地完成一道番茄炒蛋。

  1. 00010000(洗一个番茄放到 0 号碗中,我们要重复发送这个信号 3 次,因为我们需要 3 个番茄)
  2. 00010000(洗一个番茄放到 0 号碗中)
  3. 00010000(洗一个番茄放到 0 号碗中)
  4. 00100001(打一个鸡蛋放到 1 号碗中。为了实现打 4 个鸡蛋,这条得重复 4 次)
  5. 00100001(打一个鸡蛋放到 1 号碗中)
  6. 00100001(打一个鸡蛋放到 1 号碗中)
  7. 00100001(打一个鸡蛋放到 1 号碗中)
  8. 00110000(将 0 号碗中的食材全部切块,即将番茄全部切块)
  9. 01000010(点火,向锅中加入 20 mL 的油)
  10. 11000110(等待 60 秒,即 1 分钟)
  11. 11110001(将 1 号碗中的鸡蛋液倒入锅中)
  12. 10001100(翻炒 120 秒,即 2 分钟)
  13. 11111111(熄火)
  14. 11110010(将炒好的鸡蛋倒到 2 号碗中,备用)
  15. 01000001(点火,向锅中加入 10 mL 的油)
  16. 11110000(将 0 号碗中的番茄块倒入锅中)
  17. 10001100(翻炒 120 秒,即 2 分钟)
  18. 10001100(翻炒 120 秒,即 2 分钟,为了实现翻炒 4 分钟,我们只能分拆成两条指令来完成(为什么?))
  19. 11110010(将 2 号碗中炒好的鸡蛋倒入锅中)
  20. 10000110(翻炒 60 秒,即 1 分钟)
  21. 11100010(向锅中加入 2 g 盐)
  22. 10000110(翻炒 60 秒,即 1 分钟)
  23. 11111111(熄火)
  24. 11110011(将炒好的菜倒入 3 号碗中)

最终,我们可以在 3 号碗中得到一份 美味的 番茄炒蛋。如果我们把这 24 条信号记录在某种「你缺饭课一号」可以直接读取的设备上,并让「你缺饭课一号」能够自动地从这个设备逐一读取信号并执行,不就实现了整个过程的自动化运行了吗?我们把这样一条一条的信号称为「指令」(instruction),它是机器能够直接执行的最小单元。可以想象,要让一个程序在机器上运行,就需要把程序转变成一条条指令,并将这些指令按顺序装载进机器。与源代码相对,我们将一个程序所对应的指令序列称为「机器代码」,又称为「机器语言代码」。

这个「存放指令的设备」对应到计算机上就是内存。诶——你是不是想到硬盘上去啦?的确,我们的电脑程序都是安装在硬盘上的(如果不知道的话,罚你重修 认识你的电脑😡)。但事实上,你双击启动一个程序时,机器会首先把它加载到内存里,然后再从内存中一条一条取出指令运行。

在现实的计算机内部,CPU 就像上面的「你缺饭课一号」一样,不断地从内存中「取指令」,取出的指令以电信号的形式,通过「译码」来识别出功能。随后,CPU 按照设计好的规则执行相应的功能。显然,我们的计算机并不是用来炒菜的,它的本职工作就是「计算」。因此,在计算机的指令世界中,肯定不会有什么「洗菜」「翻炒」这样的指令;相反,在计算机中,常用的指令按照功能可以分成如下的几类:

  • 数据传输指令:它们用来将数据在计算机内部各种地方传递,如从内存中转移到 CPU 内部,或者将 CPU 内部计算好的结果转移到内存。在「你缺计课一号」中,我们设计有 4 个临时存放食材的「碗」和一个用来炒菜的「锅」。在 CPU 内部,也有类似的「临时存储数据」的结构,称为「寄存器」。数据传输指令的主要功能,就是将数据在内存和寄存器中转移。

    寄存器的读写速度非常非常快,远远快于内存。不过,现代的处理器中通常只有几个或几十个寄存器,每个寄存器仅能存放几十位的数据,因此对 CPU 来说,寄存器必须珍惜使用,因而需要频繁地进行数据传输。

  • 算术与逻辑运算指令:它们用来执行各种各样的计算,包括算术运算和逻辑运算。算术运算就是诸如「加减乘除」这样的数字运算,而逻辑运算指的是「或与非」这些条件的判定。

  • 比较与控制流指令:它们用来实现分支结构和循环结构,主要的功能就是「比较」「跳转」。例如,「无条件跳转」指令在执行后,CPU 就不再会继续执行下一条指令,而是转移到机器代码中的另一处开始运行;而「大于则跳转」指令则会根据两个数的大小关系来决定要不要跳转。

  • 机器控制指令:它们可以用来控制整个计算机的运转,包括停机、重置等。

随着计算机技术的不断发展,如今,我们常用的电脑 CPU 已经支持上千种不同的指令,上面所展示的只是这个庞大系统的冰山一角。为了让我们使用编程语言编写的程序能够在 CPU 上运行,就需要将源代码转换成由指令组合而成的机器代码。机器代码存放在可执行文件——也就是 .exe 文件——当中,我们双击运行一个程序,本质就是将它的机器代码装入内存然后执行。因此,我们在电脑上安装的各种各样的软件,本质就是存放着各种各样的程序的机器代码。

显然,这个将源代码转换为机器代码的过程并不轻松——在番茄炒蛋的世界里,一共只有十来种操作,但我们手工按照原始的「番茄炒蛋菜谱」编写出在「你缺饭课一号」上运行的机器代码也不是容易的事;而当这一切面对的是有上千种指令,操作的类型更是不计其数的计算机世界时,难度更是迈上了一个台阶。好在,早在上个世纪中叶,电子计算机问世之后不久,人们就设计出了自动完成这个过程的程序。借助它,机器能自动地将使用编程语言编写的源代码转换成机器代码。我们把这个过程叫做「编译」(compile),而这种程序称为「编译器」(compiler)。

编译器也是一个程序,那它自己是怎么由源代码转换成机器代码的呢?你可以这样理解:第一个编译器是人们用机器代码直接编写的,而当它发展得足够完善之后,人们就可以改用编程语言来设计它,然后由它来编译它自己。这个性质称为编译器的「自举」。

自然,不同的编程语言需要不同的编译器,同一种编程语言也可以有多种不同「品牌」的编译器。例如,C/C++ 最常用的编译器为「GCC」2「Clang/LLVM」和「微软 Visual C++ 编译器」,它们分别是今天 Linux、macOS 和 Windows 三大操作系统自身主要使用的编译器。Java 则使用「Oracle JDK」或「OpenJDK」提供的编译器——在电脑上玩过电子游戏《我的世界》(Minecraft)的读者肯定对这些名字不陌生。

有一些语言,比如 Python,则选择了另外的思路——比起事先将整个程序全部编译成机器代码,这些语言选择在程序运行时再完成转换。尽管最终实质上都将程序变成了对应功能的机器代码,但我们把这样的方式叫做「翻译」或「解释」(interpret),对应的工具则称为「翻译器」或「解释器」(interpreter)。CPython3 是 Python 最常用的解释器,也是事实上的「官方解释器」——如果你在其他地方学习过 Python 编程,你安装的那个可以在 >>> 之后给出单个命令并执行的工具,就是 CPython 解释器。

CPython

编译器和解释器解决了从「程序」到「机器代码」的跨越:借助它们的力量,我们设计的各种程序终于得以在硬件上运行。不过,我们说到这世界上的编程语言有成百上千种,那 CPU 所使用的指令难道都是一样的吗?进一步说,难道世界上所有的计算机,都使用着相同的指令设计吗?答案显然是否定的。事实上,它们不仅不一样,背后还像 浏览器 那样,有着一段「明争暗斗」的历史。

指令集的明争暗斗 #

从 8086 到 x86-64 #

在上一小节,我们构想了一台「你缺饭课一号」,这台炒菜机器有 4 个用于临时存放食材的「碗」,能够接受 8 位长度的指令来执行炒菜动作。我们在上文的表格中详细约定了每一种类型的指令的形式和功能。事实上,我们完全可以采用不同的指令格式、不一样的指令长度,甚至也可以选择再增加或者删除一些指令(比如将「点火」和「倒油」功能分拆成两种指令,或者将一些功能合并成一条新的指令),设计出「你缺饭课二号」「你缺饭课三号」……我们把这样的一种对「指令种类、形式、长度等一切相关的东西」的约定,称为「指令集架构」(instruction set architecture,ISA),简称「指令集」。

我们能自然而然地想象到,在计算机的世界里,指令集肯定也不会是只有一枝独秀的天地,而是百花齐放的花园。但这回,我们的想象与现实之间,有一点点小差距。这个故事还要从 1978 年说起。那一年,美国半导体公司英特尔推出了一款名为「8086」的处理器,它拥有 8 个寄存器,每个寄存器可以存放 16 位的数据,因此被人们称为「16 位处理器」。8086 支持约 100 种不同的指令,每种指令长度不一,最短者为 1 字节(8 位),最长者为 6 字节(48 位)。

在那个年代,与 8086 同台竞争的 CPU 数量不少,它们大都使用同样的 16 位设计,但是在指令风格、指令数量等方面有差异。按照正常的剧本来说,它应该会和竞品一起在市场上打得「有来有回」,然后一同走向历史的坟墓。可是,历史总是充满未知和巧合。当时,知名的电脑公司 IBM(在 以密码之剑护网安之城 等多个章节中,我们提到过它的名字)正苦于自己过去几年在个人电脑领域的失败中:IBM 此前拿手的是「大型机」的设计,他们传统的设计流程无法适应低端、廉价、小型的个人电脑市场。在痛定思痛的基础上,IBM 做出了一个大胆的决定:不再拘泥于自己设计 CPU 等硬件,而是把选择交给市场。这时,8086 就入了 IBM 的法眼。

「全链条覆盖」和「交给市场」是设计复杂系统的两种思路。历史已经多次告诉我们,这二者间没有固定的「谁好谁坏」的关系,我们必须结合历史条件、产业性质来具体问题具体分析。

1981 年,IBM 发布了名为「IBM PC」的个人电脑,选择 8086 的兄弟型号 8088 作为 CPU。在 8088 处理器的加持下,IBM PC 取得了性能和价格之间不错的平衡,迅速在市场上占有了一席之地。同时,IBM 还开放了 IBM PC 的技术参考资料,这意味着其他厂商都可以自由设计、生产和出售与 IBM PC 兼容的软件和硬件,甚至是设计出与 IBM PC 本身兼容的电脑。同时,微软也加入了这个 脆弱的 同盟,提供了一套稳定且实用的操作系统 MS-DOS,更是吸引来了许多软件开发商,它们纷纷将自己的软件编译成能在 IBM PC 上运行的机器代码来发布。在这样三重因素的催化之下,IBM PC 很快成为了「个人电脑」的代名词,命运的齿轮从此开始转动。

1982 年,英特尔发布了 8086 的后继产品 80286,它与 8086 使用相同的指令集,但提升了运行的主频,并增强了对内存的访问能力。而到了 1985 年,第三代产品 80386 横空出世。与 8086 和 80286 的显著区别是,80386 将 8 个寄存器的大小升级为了 32 位,因此我们称它为 「32 位处理器」。不过,英特尔并没有选择让它与 8086 / 80286「割席」,而是通过在它们原本的指令集上额外引入一批指令来支持 32 位相关的功能。这意味着原本能在 8086 / 80286 上运行的软件可以无需重新编译直接使用,同时以后发布的新软件则可以选择针对 32 位特性引入更强大的功能。这种「向后兼容性」使得 80386 再一次颠覆市场,再加上便捷易用的 Windows 操作系统的「横空出世」,8086 / 80286 / 80386 这三代处理器所使用的指令集逐渐成为了「事实上」的电脑 CPU 标准。由于这几代产品都使用「80x86」来命名,人们把这套指令集命名为「x86」。

x86 的巨大成功还吸引来了其他一些 CPU 厂商,它们从英特尔那里拿到授权,生产自己设计的,但是使用 x86 指令集的处理器。这其中就包括 AMD。1991 年,AMD 发布了 80386 的「复刻版」Am386,随后又发布了 80486 的复刻版 Am486。它们在指令集方面与 80386 / 80486 完全兼容,凭借更低的价格开始有力地与英特尔竞争。不过,尽管 AMD 主打价格优势,但是这种总是跟在别人后面模仿的商业路线注定无法成功——芯片技术发展的速度远远快于人们的想像,等复刻品出来,「正品」都更新一代了。如果按照剧本发展的话,AMD 应该会最终没落于众多 x86 兼容芯片厂商之中,在历史的长河里留下不轻不重的一笔。

然而,转机发生在新世纪来临前的黎明。随着信息技术的飞速发展,人们发现 32 位设计已经有些「捉襟见肘」:32 位的寄存器只能存放最大为 \(2^32-1=4\,294\,967\,295\) 正整数,而当计算任务超过这个数字时,就只能采用拆分或其他的方法,这很大程度上影响了计算效率。同时,受制于同样的原因,32 位架构最大只能使用 4 GB 的内存,尽管在 2000 年前后,4 GB 内存已经是一个巨大的数字,但是当时的人们已经卓有远见地看到了未来。因此,引入新的 64 位架构,成为包括英特尔在内的有志之「司」都开始考虑的问题。

我们回看从 80286 到 80386 的 16 位向 32 位的跨越,当时英特尔选择的是在原有指令集的基础上添加新的指令,这样可以在兼容以前的老旧应用时,增加新的功能。然而,在 32 位迈向 64 位这一次,英特尔把步子迈得太大了——他们直接重新设计了一套纯 64 位的指令集(称为 IA-64)。这 IA-64 采用了新的设计思想,支持包括高效的并行计算在内的许多新功能。唯一的问题是:它与 x86 不兼容。原本的各种软件都需要换用 IA-64 的编译器重新发布新的版本。IA-64 一经发布,世人反应冷淡,这一次英特尔没能成功。

一直以来,我们都经常看见「官方逼死同人」的事情发生,不过这一次,是「同人倒逼官方」。「既然英特尔自己搞 64 位弄砸了,那不正是我 AMD 的好时机吗?」于是,AMD 在 x86 的基础上,直接把原有的 8 个寄存器扩充成 64 位,再引入一批针对 64 位操作的新指令,不仅实现了 64 位的功能,还在最大程度上保持了对原有 32 位应用的兼容性。2003 年,AMD 发布 Athlon 64,这是第一块 64 位的 x86 处理器,一经推出,好评不断,随后的两年间,支持 64 位的操作系统和应用软件就开始出现在市场上了。AMD 给这种指令集改了个名,叫「AMD64」。这是一次「同人」作品的胜利。

此时的英特尔有点尴尬——自己推出的 IA-64 无人问津,而身为「同人作者」的 AMD 反而给自己的 x86 架构升级了。不过,日子还得过嘛……2004 年,英特尔发布了具有里程碑意义的「奔腾 4 Prescott」(Pentium 4 Prescott)处理器。它的里程碑意义之一,就是使用了 AMD64 架构,成为了英特尔的第一款 64 位的 x86 处理器。曾经,人们总认为 AMD 的产品是英特尔的仿制器,可是这回,同人把官方倒逼了。

当然,英特尔肯定不会在明面上使用「AMD64」这个名字。他们把它改名成了「Intel 64」。世人一想,你们这各自拉扯,我们到底应该用哪个名称呢?于是,人们索性用「x86-64」来表示这种「64 位的 x86 指令集」,以和原本纯 32 位的 x86 作区分。后来,人们也会把「x86-64」简称为「x64」,用来表示这种指令集。后来的故事,就是在本作品的第一章 认识你的电脑 中所介绍的了那样了——英特尔和 AMD 成为了 x86-64 指令集的两大霸主,开始统治着全世界的电脑 CPU 市场,直到今天。

今天,x86-64 的指令数量已经达到了上千条,是一套名副其实的「复杂指令集」。这其中的一个原因便是 x86 指令集自 8086 诞生以来,就一直采用「向后兼容」的进化模式:每一次更新都是在之前的基础上增加功能,因此旧的软件都基本能在新硬件上直接使用——在今天的最新电脑上,许多近二十年前的软件(比如 Office 2003)仍然能正常运行。这也是 x86 家族指令集能够在桌面电脑领域占据「绝对优势」的重要原因。

得益于这种兼容性,64 位的 CPU 上既可以运行 64 位的操作系统,也可以运行 32 位的操作系统。而在 64 位的操作系统下,也可以同时存在 32 位和 64 位的软件。目前,几乎所有的电脑都在 x86-64 上运行 64 位的操作系统,不过仍然有许多 32 位的软件正在和 64 位的软件共处。打开【任务管理器】,展开【详细信息】并转到【进程】页面,正运行在 32 位的软件后方会用【(32 位)】标出。

任务管理器中的 32 位标记

对于大多数读者的电脑,右键桌面上的【此电脑】选择【属性】,就能在【设备规格】一栏下方的【系统类型】看见这样一句话:

64 位操作系统, 基于 x64 的处理器

其中「64 位操作系统」说明操作系统本身,以及其上运行的大部分软件都是 64 位的;而「基于 x64 的处理器」,说的就是 x86-64。

系统设置中的指令集说明

统治移动世界的 ARM #

尽管自 8086 开始,x86 指令集就在个人电脑领域快速抢占市场。然而,「计算机」不止局限于我们桌子上的个人电脑——无论是手机、平板电脑、MP3 这一众「智能设备」,还是日用的电视机遥控器、电高压锅、洗衣机等诸多电器,它们的内部也拥有像电脑一样的 CPU 来实现自动控制和人机交互,因而也是一种形式的「计算机」(称为「嵌入式计算机」)。而在这片天地属于移动和嵌入式设备的天地中,又是另一片勃勃生机的境界。

1985 年,来自英国的艾康电脑公司(Acorn Computers)发布了一款称为「ARM1」的原型处理器,「ARM」的含义是「Acorn 精简指令集机器」(Acorn RISC4 Machine)。作为一款 32 位的处理器,它与同时代包括 80286 在内的 CPU 最大的不同是:它具有极为简单的设计,只有约 25 条基本指令,并且舍弃了许多其他处理器中的复杂设计。这种指令集就被称为「ARM」指令集。次年,用于量产的 ARM2 投产,精简的设计使得它只需消耗极少的电能,就能有出色的性能表现。这为它日后在便捷式设备和微型设备中的大放异彩埋下了伏笔。

时间来到了 90 年代。那时,苹果公司正计划开发一款可以拿在手上触控操作的「掌上电脑」——可以看成今天 iPad 的前身。低功耗的 ARM 处理器与苹果的产品构思不谋而合。于是,1990 年,苹果、安谋电脑和另一家公司进行了分割重组,直接成立了一家名为「ARM」的公司,中文常译为「安谋」。这「ARM」的名字直接来源于 ARM 系列处理器和 ARM 指令集,不过被重新解释为「高级精简指令集机器」(Advanced RISC Machines)。1993 年,苹果推出了著名的掌上电脑「Apple Newton」(中文常称为「牛顿」),使用的就是 ARM 610 处理器。

在这几年的时间里,尽管 ARM 处理器的性能不断提升,但是其精简的结构设计和低功耗的特点没有改变。安谋敏锐地嗅到了这种低功耗 CPU 市场的商机,开始从「造芯片」转向「卖解决方案」——即,安谋自己不再进行 CPU 的制造和出售,而是只进行 ARM 指令集的升级、维护,并在 ARM 指令集上设计出芯片的原型,出售给其他的芯片公司。其他公司在购得授权后,可以在这原型上进行扩展、组合,并最终设计、制造出自己品牌的,采用 ARM 指令集的 CPU。站在后来者的视角,我们不得不感叹这种模式的强大之处:这让越来越多的企业得以入局市场,带来良性的竞争和发展,而安谋则只需依靠授权费用就能大赚一笔,又得以继续让 ARM 指令集变得更加强大。

进入新世纪以来,移动设备和嵌入式系统迎来崛起。如果说 x86(以及 x86-64)是事实上的个人电脑指令集标准,那么 ARM 无疑则是事实上的移动设备指令集标准。一方面,高通、三星、联发科、德州仪器等老牌芯片厂商「入局」ARM,让 ARM 指令集的 CPU 选择越来越多;另一方面,智能手机、平板电脑等便携式电子设备的快速发展,又反过来促进了相关芯片产业的发展。

今天,ARM 指令集的 CPU 在我们身边无处不在。无论是 iPhone 使用的 A 系列 CPU,还是各品牌安卓手机所使用的「高通骁龙」「联发科天玑」等系列 CPU,又或是华为手机使用的「海思麒麟」CPU,它们都使用的是 ARM 指令集。任天堂的掌上游戏机 Nintendo Switch 使用的是 NVIDIA Tegra X1 CPU,同样采用 ARM 指令集。而在许多小型智能设备,如门禁机、打卡机、扫地机器人中使用的「STM32」系列芯片,也使用的是 ARM 指令集。说 ARM 指令集「统治」了移动世界,一点都不为过。

ARM 指令集是一个非常「灵活」的指令集,面向不同的应用场景,它有许多不同的版本,可以对各种功能进行「选配」。例如,用在打卡机上的 ARM 指令集,肯定就要比用在智能手机上的要简单得多。

看到这儿,我们自然而然会想到一个问题:现在,手机和平板电脑的性能已经非常强大,玩游戏、剪视频,甚至轻度的办公都可以胜任,这证明 ARM 能在低功耗的同时具有非常高的「上限」,并不是只能被局限在移动设备上。反观 x86-64,厚重的「历史包袱」似乎让它有点儿喘不过气。那为什么在个人电脑领域,仍然是 x86-64 的天下呢?这个问题我们能想到,安谋肯定也能想到,高通、华为、苹果这样的芯片设计公司显然也能想到。只是,解决这个问题的答案,又要回到「软件」头上。在下一节「应用生态」,我们会继续介绍这个问题。

用开放拥抱自主 #

无论是 x86-64 还是 ARM,它们都有一个特点:受商业公司控制。虽说除了英特尔和 AMD 之外,仍然有数家企业取得了一部分 x86-64 的授权,但是它们的市场都可以视为「忽略不计」,而当下英特尔和 AMD 这俩巨头肯定也不会舍得将蛋糕再分给他人。而在 ARM 这边,想要获得 ARM 指令集的授权,也需要向安谋支付高昂的授权费用。在软件的世界里,我们有「自由软件」这样开放共享的存在,那在指令集的世界中,是否存在类似的想法呢?答案是肯定的。

2010 年,加州大学伯克利分校提出了一套名为「RISC-V」指令集。这是一套「开源指令集」,在一定的条件下,它可以被自由地用于任何目的,允许任何人设计、制造和销售使用该指令集的芯片和软件而不必支付任何专利或授权费用。在公开之后,RISC-V 指令集获得了来自许多高校、企业和研究机构的关注——毕竟,和自由软件的「众人拾材火焰高」一样,共同建设 RISC-V 指令集,最终亦能使自己受益。十多年来,RISC-V 的关注度一路高涨,并且也已经有了商业的和开源的芯片实现,如阿里巴巴推出的玄铁 910 CPU,以及中科院的「香山」CPU。尽管目前对 RISC-V 的实际应用仍然处于实验室阶段,但它的表现已经未来可期。

除了 RISC-V 之外,我国芯片公司龙芯也在 2021 年发布了一套全新的指令集「LoongArch」,中文称「龙架构」,并稳步推出了几代 CPU 产品,均具有极为出色的性能表现。龙架构除了基础部分开源外,还有一个更为重要的性质——「自主」。例如,尽管理论上只要购买授权就能设计出自己的 ARM 指令集处理器,但是「卖不卖给你」这事儿,也得由人家说了算。2019 年,中美贸易摩擦正激烈的时候,网上就有人开始讨论 ARM 是否会停止对华为的授权。尽管目前华为拥有 ARMv9 的永久授权,但当下的国际形势风云变幻,谁也无法预料「脱钩」是否真的会发生。这不断地提醒着我们:自主,很重要。

就像龙架构的诞生一样,「开放」和「自主」并不矛盾;相反,它们在很多时候,是一体两面的统一体。用开放拥抱自主,是当下处于变局之中的我们最好的选择。

应用生态与转译 #

在介绍 ARM 指令集时,我们留下了一个疑问:明明 ARM 指令集的上限很高,功耗又更低,没有那些「历史包袱」,可为什么今天在个人电脑领域,并没有与 x86-64「平分秋色」?进一步说,RISC-V、龙架构这些新兴指令集,为什么难以在市场上推进?一切的根源便是由「软件」所构造起来的「应用生态」。

由于不同指令集的 CPU 采用了不一样的指令设计,原先为一种指令集编译的软件,无法直接在另一种指令集的处理器上运行。例如,在 ARM 指令集的 CPU 上,无法直接运行 x86-64 的机器代码。这使得使用指令集的 CPU 之间形成了一层层厚厚的「墙」,软件难以直接跨越。我们把为某个指令集编译的所有软件组成的整体,称为这个指令集的「应用生态」。

显然,由于 x86 架构在历史上的成功,在 x86(和 x86-64)指令集上,已经构建起了巨大无比的应用生态。从 Windows 操作系统本身开始,我们所使用的几乎一切电脑软件,都提供了为 x86 或 x86-64 指令集编译的机器代码。我们能够在网上直接下载一款 app 并双击运行,就是因为大家已经默认所有人都在使用 x86-64 指令集的 CPU。如果我们需要改用 ARM 指令集的 CPU,最根本的办法,就是要求所有软件厂商,使用针对 ARM 的编译器,重新编译它们的软件。这显然有一定的难度。

如果存在比较封闭的软件生态(即常用软件的数量不大、且软件厂商可控),只要有足够的时间,这么做并非完全不可行。2020 年,苹果发布了采用 ARM 指令集的电脑处理器 Apple M1,并推出了搭载该芯片的电脑——此前,苹果的电脑产品亦使用英特尔的处理器。由于本身在 macOS 上的软件支持就相对较少,同时苹果又有极强的行业号召力,到了今天(2024 年),在 ARM 指令集上的苹果电脑已经有了相当数量的软件支持。又得益于 ARM 指令集较为高效的设计,这些电脑产品在日常办公、平面设计乃至软件开发等少数专业领域已经有了不错的体验。

但这样的成功难以在 Windows 平台上复刻——面向 Windows 平台的软件多如牛毛,即使微软再有能力,也不可能亲自逐个联系他们,要求使用针对 ARM 的编译器重新编译软件。既然让软件厂商在一夜之间全部完成转变不太现实,人们想出了另一种折中的方案:能不能设计一种「现场翻译」,当需要在一种指令集上运行另一种指令集的机器代码时,一句一句将指令「翻译」为本指令集的指令呢?这就是「转译」。

实际上,苹果在推出使用 ARM 指令集电脑的同时,亦发布了一款称为「Rosetta 2」5的转译器,对于那些过去编译的、针对 x86-64 指令集的应用,macOS 使用该转译器来在 ARM 指令集的处理器上运行。这为软件厂商的转变腾出了宝贵的「窗口期」。然而,转译最大的问题便是性能:可以想象,原本一条指令就能执行的事,由于转译本身存在的开销,就要变成许多条指令才能完成了。同时,转译并不是完美的——如果一些软件的设计本身就依赖 x86-64 指令集的特性,那转译也无能为力。

如果说苹果借助自己的影响力和 Rosetta 2 转译器,实现了差强人意的从 x86-64 到 ARM 的过渡,那这个问题在 Windows 身上,就不那么顺利了。尽管 Windows 自身早已推出了支持 ARM 指令集的版本,但是在应用软件方面,Windows 要比 macOS 更加依赖转译得多——然而,转译带来的问题是客观存在的,这注定造就了在 ARM 指令集上,Windows 的软件生态更难构建。直到今天,虽然市面上已经可以见到少量采用 ARM 指令集 CPU 的 Windows 电脑6,但它们的使用体验仍然有相当大的提升空间。

编程能为我们带来什么 #

现在,相信对「程序是什么」「编程在做什么」以及「程序如何在电脑硬件上运行」三个问题,你在心中已经有了答案。让我们来进入最后一个问题,也是一个「现实」的问题:今天,各种各样的「编程培训」广告无处不在,从小学生到职场人士,似乎都在学习「编程」。

(这节未完成。主要内容:解决实际问题,如使用 Python 连接 Excel 助力办公;培养逻辑思维。)

练习 #

(待完成)


  1. 我们使用 NGINX。它会在你访问我们的网站时,把网页的内容发送给你。 ↩︎

  2. GCC(全称「GNU Compiler Collection」,译作 GNU 编译器套装)包含一系列针对多种语言的编译器,如 C、C++、Objective-C 等)。 ↩︎

  3. 之所以叫「CPython」是因为它是用 C 语言写的。这也提醒了我们,一种编程语言的编译器(解释器)并不一定要用这种语言自身来编写。 ↩︎

  4. 是「精简指令集电脑」(Reduced Instruction Set Computer)的缩写。 ↩︎

  5. 为什么是「2」呢?其实苹果电脑在本世纪出还经历过一次指令集转换:从「PowerPC」指令集迁移到 x86-64。当时苹果也推出了一款转译器,称为「Rosetta」。顺带一提,Rosetta 一名来源于古埃及的罗塞塔石碑,其上刻有同一段内容的三种不同语言版本。它的出土让考古学家得以解读已经失传千年的埃及象形文字。 ↩︎

  6. 这些电脑通常使用高通的处理器。 ↩︎