计算机的工作由程序驱动,而程序需要用编程语言来编写。从最初直接书写由 0 和 1 组成的指令,到用符号代替二进制编码,再到用接近自然语言的方式描述任务,编程语言先后经历了三个发展阶段,编写程序也随之变得越来越容易。
沿着这条演进脉络,本篇将依次讲解机器语言、汇编语言与高级语言这三代编程语言的核心特征,梳理演进的内在逻辑,并说明程序从代码到执行的完整过程,为后续正式编写 Python 程序打好基础。
1 编程语言的演进
1.1 机器语言
计算机硬件能够直接识别和执行的,只有由 0 和 1 组成的二进制指令序列。为什么偏偏是 0 和 1?这与硬件的构成方式有关:计算机硬件由数字电路构成,对电路而言,最容易稳定区分的是低电平与高电平两种状态。人们约定用 0 表示低电平、用 1 表示高电平,0 和 1 便与这两种状态一一对应,电路无须分辨更多状态就能可靠工作。
指令以二进制编码的形式存放在存储器中,CPU 取出指令后将其译码,再由内部的数字电路完成相应操作。这种由二进制序列写成的指令集合,称为机器语言(Machine Language),它是计算机历史上的第一代编程语言。
机器指令通常由两部分构成:操作码与地址码。操作码指明要执行的操作类型,如加法、传送等;地址码给出要处理的数据,或指明数据存放的位置。一条指令的具体长度(即二进制位数)与编码格式,由所用的 CPU 架构决定。
下面给出一段机器语言程序,其任务是计算 3 与 5 的和,并把结果保存到内存中。示例中的每条指令写为 16 位,并按 8 位一组分成两段——分段与空格只是为了便于阅读,真实的指令在机器中是一串连续的 0 和 1:
10100000 00000011 10100001 00000101 10110000 00000001 11000000 00010000这四条指令对应的操作依次是:
- 把数值 3 载入寄存器 A。
- 把数值 5 载入寄存器 B。
- 把寄存器 B 的值加到寄存器 A 上。
- 把寄存器 A 的值存入内存地址 16。
四条指令依次执行完毕后,寄存器 A 与内存地址 16 中保存的都是 3 与 5 的和,即 8。
这些指令都提到了寄存器。寄存器(Register)是 CPU 内部用来临时存放数据的存储单元,存取速度远高于内存,CPU 的运算主要围绕它进行。
需要说明的是,上述编码是为讲解而设计的示意写法,并非某款真实处理器的实际指令编码,但它足以反映机器语言的基本面貌。
作为第一代编程语言,机器语言具有以下核心特征:
- 执行效率极高:机器指令无须任何转换环节,可直接被 CPU 执行。
- 硬件依赖严重:机器语言深度绑定特定处理器的指令集架构(Instruction Set Architecture,简称 ISA)。不同架构(如 x86 与 ARM)的指令编码规则互不兼容,为一种架构编写的机器语言程序,无法直接在另一种架构的机器上运行。
- 开发成本高昂:开发者需要记住每条机器指令的二进制编码及其含义,编写与阅读都极易出错,错误也难以排查;即便只编写一个很短的程序,也要耗费大量时间。
1.2 汇编语言
机器语言的根本困难在于:人类必须以二进制形式直接操控硬件。要在程序中完成一次载入操作,就得写出 10100000 00000011 这样的序列,这要求开发者熟记指令的编码方式,认知负担相当沉重。为了摆脱这一困境,人们改用便于理解的符号来书写指令,由此诞生了第二代编程语言——汇编语言(Assembly Language)。
汇编语言最核心的变化是引入助记符(Mnemonic):用 LOAD、ADD、STORE 这样的英文缩写表示载入、加法、存入等操作。不仅如此,寄存器与内存地址也用 A、B、16 这样的符号和数字表示,数据可以直接写成十进制,开发者不必再书写 0 和 1 的长串序列。由于采用符号化的表达方式,汇编语言也常被称为符号语言(Symbolic Language)。
把前面示例中的四条机器指令转换为对应的汇编代码,语义结构会清晰许多:
LOAD A, 3 LOAD B, 5 ADD A, B STORE A, 16与前面的二进制序列相比,汇编代码的可读性明显提升。当然,不同架构的汇编语法各有差异,此处写法仅作示意。
然而,CPU 只能识别二进制形式的指令,无法直接执行 LOAD 这样的符号。因此,需要借助一个专门的翻译程序——汇编器(Assembler),把汇编代码逐条翻译为对应的机器指令,CPU 执行的正是翻译后的二进制指令。
尽管可读性有所改善,汇编语言与机器语言仍同属低级语言(Low-level Language):一条汇编指令通常只对应一条机器指令,开发者仍然需要直接面对寄存器、内存地址等硬件层面的概念。
汇编语言的核心特征可以概括为以下三点:
- 底层控制力强:汇编语言允许开发者精确控制 CPU 寄存器与内存地址,能够生成十分精简的二进制指令。在对性能和资源占用有极高要求的场景(如操作系统内核、设备驱动程序、嵌入式实时系统)中,汇编语言至今仍在使用。
- 平台依赖明显:汇编指令与特定的指令集架构强绑定,为 x86 架构编写的汇编程序无法直接在 ARM 架构上运行,代码复用率通常较低。
- 开发门槛偏高:编写汇编程序要求开发者对目标计算机的底层硬件有较深入的了解,否则难以写出正确的程序。
1.3 高级语言
汇编语言解决了可读性问题,却没有解决硬件依赖问题。用汇编语言编写程序,仍需关注寄存器如何分配、数据存放在哪个内存地址——这些细节与要解决的实际问题(如计算一个公式、处理一批数据)并无直接关系。为了解决这一问题,人们创造出了第三代编程语言,即高级语言(High-Level Language)。这类语言对底层硬件细节的抽象程度远高于前两代:开发者无须再直接操控寄存器与内存地址,能够把精力集中于待解决的任务本身。
高级语言提供贴近人类自然语言与数学表达的语法结构。你只需描述 “做什么”,至于 “怎么做”,则交给相应的翻译程序完成,承担这一工作的主要是编译器与解释器,它们的工作机制将在下文详细讲解。
假设要计算 3 与 5 的和,并把结果显示到屏幕上。这一任务若交给汇编语言,前面示例中的程序只能完成求和与保存的部分;要把结果显示出来,还得把数值转换成一串字符,再调用操作系统的输出功能。而在 Python 中,同样的任务只需两行代码:
total = 3 + 5 print(total)即使从未接触过 Python,你也不难猜出这两行代码的意图:第一行计算 3 与 5 的和,并把结果存入 total;第二行把 total 的值输出到屏幕。至于数据在底层如何存放与传递、屏幕上的字符如何生成,Python 都会自动安排妥当,你只需关注 “求两数之和并输出” 这一任务本身。这种高度抽象,正是高级语言大幅提升软件开发效率的重要原因。这两行代码涉及的语法知识,此处暂不展开,后续文章将详细讲解。
高级语言的另一项关键优势是跨平台能力:用高级语言编写的程序,通常只需为不同平台选用相应的编译器或解释器,即可在多种硬件架构上运行,这与机器语言、汇编语言 “一种架构一套代码” 的局面截然不同。
高级语言种类繁多,下表列出了业界具有代表性的语言及其核心特征与应用领域:
| 语言名称 | 核心特征与应用领域 |
|---|---|
| Python | 语法简洁清晰,贴近自然语言,在数据分析、人工智能与自动化脚本领域应用广泛。 |
| C | 通用过程式编程语言,以高效与灵活著称,广泛应用于操作系统内核、嵌入式系统及底层驱动开发。 |
| C++ | 在 C 语言基础上扩展了面向对象与泛型编程特性,常用于高性能计算与大型基础软件。 |
| Java | 面向对象的跨平台语言,借助 Java 虚拟机实现 “一次编写,到处运行”,广泛用于企业级后端服务。 |
| C# | 微软推出的多范式语言,依托 .NET 平台运行,常用于 Windows 应用、Web 服务与游戏开发。 |
| JavaScript | Web 交互的核心脚本语言,依托 Node.js 亦可运行于服务端,是现代 Web 前后端开发的基础。 |
| TypeScript | JavaScript 的超集,增加了静态类型系统,适用于大型 Web 应用开发。 |
| Go | 语法极简且原生支持高并发,常用于云计算基础设施与微服务架构。 |
| Rust | 以内存安全与高性能著称,常用于操作系统、命令行工具等系统级程序的开发。 |
此外,还有用于苹果平台的 Swift、面向 Android 开发的 Kotlin、专注统计计算的 R、广泛用于服务端开发的 PHP、面向关系型数据库的结构化查询语言(Structured Query Language,简称 SQL)等,它们同样应用广泛,各有专长。
1.4 三代语言的对比与演进逻辑
把三代语言放在一起对比,各自的特点与演进方向一目了然:
| 对比项 | 机器语言 | 汇编语言 | 高级语言 |
|---|---|---|---|
| 表达形式 | 二进制序列 | 助记符与符号 | 贴近自然语言与数学表达的语法 |
| 可读性 | 极低 | 有所改善 | 高 |
| 与硬件的绑定程度 | 深度绑定特定架构 | 强绑定特定架构 | 通常与硬件平台无关 |
| 翻译环节 | 无须翻译,CPU 直接执行 | 由汇编器翻译为机器指令 | 由编译器或解释器翻译为机器指令 |
从这张表可以看清演进的内在逻辑:每一代语言的出现,都是为了解决前一代最突出的问题——汇编语言用助记符化解了机器语言的可读性难题,高级语言则把寄存器与内存地址的安排等硬件细节交给了编译器或解释器处理。经过三代演进,程序的表达方式越来越接近自然语言,与具体硬件的关联越来越弱。
不过,演进并没有让前两代语言失去作用。直到今天,三代语言在计算机系统中依然各司其职:任何程序的运行,最终都要落实到一条条机器指令,由 CPU 执行;对性能要求极高的场合,仍离不开汇编语言;而绝大多数应用软件,则由高级语言开发。
2 程序的执行机制
高级语言把 “怎么做” 交给了相应的翻译程序。但翻译本身大有讲究:在什么时机翻译?按什么方式翻译?不同的选择,直接影响程序的执行效率与跨平台能力。
在展开分析之前,先明确两个基本概念:用高级语言写出的程序代码,称为源代码(Source Code);翻译的最终产物——CPU 可以直接执行的二进制指令序列,称为机器码(Machine Code)。源代码无法被 CPU 直接执行,必须先翻译成机器码,程序才能真正运行。本节将依次分析编译型、解释型与混合型三类执行方式,最后回到 Python 本身,考察它的底层执行机制。
2.1 编译型语言
编译型语言(Compiled Language)以 C、C++ 为典型代表,采用“预先整体翻译”的执行方式:
- 翻译机制:程序运行之前,由编译器将全部源代码一次性翻译为特定平台的机器码,并生成独立的可执行文件。
- 技术优势:翻译工作在运行前已经完成,程序执行时没有任何翻译开销,运行效率高。这类语言通常应用于操作系统内核、游戏引擎等对性能要求严苛的领域。
- 技术局限:跨平台能力受限。生成的可执行文件深度绑定特定的指令集架构与操作系统,若要在不同平台上运行,通常需要针对目标平台重新编译。
2.2 解释型语言
解释型语言(Interpreted Language)以 JavaScript 与 Python 为典型代表,采用“边翻译边执行”的方式:
- 翻译机制:程序运行时,由解释器逐条读取源代码,翻译一条、执行一条。
- 技术优势:天然具备跨平台特性,只要目标平台安装了相应的解释器,同一份源代码就可以直接运行;程序修改后无须等待编译,可以立即执行,调试反馈十分及时。
- 技术局限:翻译发生在运行期间,且每次运行都要重新翻译,因此整体运行效率通常低于编译型语言。
2.3 编译与解释的对比
两种执行方式的差异,可以汇总为下表:
| 对比项 | 编译型语言 | 解释型语言 |
|---|---|---|
| 翻译时机 | 运行前一次性整体翻译 | 运行时逐条翻译并执行 |
| 运行效率 | 高,执行时没有翻译开销 | 通常较低,翻译与执行同时进行 |
| 跨平台能力 | 受限,通常需针对目标平台重新编译 | 只需目标平台安装相应的解释器 |
| 修改源代码后 | 需重新编译,方可运行新版本 | 无须重新编译,可立即再次运行 |
| 代表语言 | C、C++ | JavaScript、Python |
💡 提示:编译与解释的类比
- 编译型语言好比出版译本:一本中文原著,想给日本读者看,就要先完整翻译出日文版再出版;想给韩国读者看,就要另外翻译出版韩文版。读者读译本又快又顺畅,但每种语言的译本都要单独翻译。
- 解释型语言则好比同声传译:译员听一句、译一句,台下无论坐着哪个国家的听众,只要配上相应的译员,同一份原稿都能现场传达。只是听译的速度,通常不及阅读译本。
2.4 混合执行:字节码与虚拟机
编译型语言执行效率高,解释型语言跨平台灵活,二者各有长短。为了兼顾这两种能力,许多编程语言把编译与解释结合起来:在源代码与机器码之间引入一个独立于硬件架构的中间层——字节码(Bytecode)。源代码先被翻译为字节码,再交由一种称为虚拟机(Virtual Machine)的专门软件执行。Java 与 Python 是其中最具代表性的两个例子。
Java 的执行流程
Java 的执行流程分为两步:
- 显式编译:Java 源代码(.java 文件)首先由编译器(如官方工具 javac)编译为字节码文件(.class 文件)。这一步由开发者主动执行,可以清楚感知,因此称为 “显式” 编译。
- 虚拟机执行:程序运行时,位于操作系统之上的Java 虚拟机(Java Virtual Machine,简称JVM)负责加载字节码文件,把字节码逐条翻译为当前平台的机器码并执行。字节码不依赖任何具体硬件,同一份字节码可以运行在任何安装了 JVM 的平台上,这正是 Java “一次编写,到处运行” 的实现基础。
📚 扩展:即时编译器
逐条解释执行字节码,难免带来性能开销。为了弥补这一短板,现代 JVM 普遍集成了即时编译器(Just-In-Time Compiler,简称JIT):它在运行期间持续监控,一旦发现被高频执行的热点代码,就将其直接编译为机器码并缓存起来,后续调用无须再逐条翻译,热点代码因此能以机器码的速度运行。
Java 从源代码到执行的完整流程如下图所示:
Python 的执行流程
尽管业界习惯把 Python 称为解释型语言,但它的底层执行同样经历了编译过程,只是整个过程对开发者高度透明。以 Python 的官方实现 CPython 为例,一段 Python 代码的执行分为两步:
- 隐式编译:解释器接收到源代码后,会首先自动将其编译为字节码。这一步无须开发者进行任何操作,因此称为 “隐式” 编译。
- 虚拟机执行:字节码随后交由Python 虚拟机(Python Virtual Machine,简称PVM)执行。PVM 是 Python 解释器的核心执行引擎,负责逐条读取字节码指令并执行相应操作,最终完成程序的计算任务。
📚 扩展:字节码的磁盘缓存
对直接运行的程序而言,编译得到的字节码只存在于内存中,程序结束便随之释放。而当一个代码文件被程序中的其他文件使用时,Python 会把编译得到的字节码缓存到该文件所在目录的__pycache__文件夹中,扩展名为.pyc;此后再次使用时,只要源代码没有改动,解释器就直接加载这份缓存,省去重新编译的开销。
Python 从源代码到执行的完整流程如下图所示:
两种实现的对比
Java 与 Python 的核心流程是一致的:源代码先被编译为字节码,字节码再交由虚拟机逐条执行。差异主要体现在两处:
- 编译主体与字节码的去向不同:Java 的编译由开发者主动执行,编译得到的字节码以文件形式保存在磁盘上;Python 的编译则由解释器在幕后自动完成,字节码通常只存在于内存中。相应地,Java 代码修改后需要重新编译才能运行,Python 代码修改后可以立即再次执行。
- 优化策略不同:Java 普遍借助即时编译器加快字节码的执行;Python 则依靠磁盘缓存减少重复编译,其字节码始终由 PVM 逐条解释执行——这也是它运行效率相对有限的直接原因之一。
可见,混合执行并非一套固定不变的流程:编译由谁执行、字节码存放在哪里、优化从何处入手,都可以按需取舍。对使用者来说,Python 的编译过程完全透明,修改代码后可以立即运行,这种使用体验与解释型语言十分接近;业界习惯把 Python 称为解释型语言,正是对这一使用体验的通俗概括,而不是对其底层机制的精确描述。
📝 总结:从编写到执行
本篇回答了两个问题:程序用什么语言编写,程序如何真正运行。编程语言先后经历机器语言、汇编语言与高级语言三个发展阶段,表达方式越来越贴近人类,对硬件的依赖越来越小;但无论用哪种语言编写,程序最终都必须以机器码的形式由 CPU 执行。围绕 “翻译” 这一环,编译型语言在运行前一次性完成翻译,执行效率高而跨平台受限;解释型语言边翻译边执行,跨平台灵活而运行效率通常较低;混合执行借助字节码与虚拟机,在执行效率与跨平台能力之间取得平衡。Python 采用的正是混合执行:源代码先由解释器自动编译为字节码,再由 Python 虚拟机逐条执行。