跳至内容
Topos Uranos

摘要

本课说明什么是程序、编译器做什么:机器只对内存中的数执行基本运算,源代码必须先翻译成机器码才能运行。本课借助一个由两个文件组成的程序,走过 C++ 中这一翻译的三个阶段(预处理、编译和链接),并区分编译时发生的事与运行时发生的事,每类各举一个错误。本课还比较编译型语言与解释型语言,概述 C++ 直到 C++23 和 C++26 标准的历史,并明确本课程所教的是怎样的 C++。

学习目标

  1. 借助示意图说明一个源文件变成可执行文件的过程:预处理、编译和链接,以及每个阶段的输入和输出。
  2. 判断一个事件属于编译、链接还是运行,例如语法错误、缺少函数或除以零。
  3. 按照错误被发现的时刻以及编程者能看到多少机器,比较编译型语言与解释型语言。
  4. 按时间顺序排列语言的各个标准,并说明本课程为什么使用 C++23。

什么是程序

程序是机器能够执行的一系列指令,用来把一些数据变换为一些结果。上一课从问题到算法说明了程序之前有算法,即解决问题的方法,它是写给人看、供人遵循的。程序则是同一方法写给机器的形式;机器不懂句子,也不懂意图,只懂基本命令:把一个数从内存的一处复制到另一处,把两个数相加,比较它们,根据结果跳转到另一条指令。

计算机的内存是一长排编了号的存储单元,每个单元存放一个 0 到 255 之间的数(一个字节)。机器处理的一切都以数的形式编码在那里:数据如此,指令本身也如此。既然如此,可执行程序就是处理器当作命令来解读的一串数,即机器码,而且它依赖于处理器:台式计算机中常见的 x86-64 系列处理器,与手机和较新的 Mac 中常见的 ARM 处理器,认得的数并不相同。

如今没有人用机器码写程序。人们用高级语言(例如 C++)写程序,它的指令近似于数学记号,写在一个称为源代码(或简称源文件)的文本文件中。由此可知,需要一个翻译者,根据源代码生成等价的机器码:在 C++ 中,这个翻译者就是编译器。

例题一个程序与机器执行的内容

这是一个完整的 C++ 程序。现在还不必理解它:它的每一行都将在第 1.5 课中讲解。只需看到它是文本,可以阅读,而下方显示的是它运行时输出的内容。

C++ 程序
// 第 1.5 课将逐行讲解。#include <print>​int main(){    std::println("你好,世界");    std::println("2 + 3 = {}", 2 + 3);}
输出
你好,世界2 + 3 = 5

为了看到翻译的另一端,考虑一个只把两个整数相加的函数(函数将在第 5 单元学习)。它不是完整的程序,因为缺少 main,即开始执行的地方,但编译器可以单独翻译它。

C++ 代码
int add(int a, int b){    return a + b;}

程序 objdump 显示 GCC 15 在一台运行 Linux 的 x86-64 计算机上为它生成的机器码:每一行依次是指令的位置、以十六进制表示的字节和便于阅读的指令名。

终端
$ g++ -std=c++23 -O1 -c add.cpp$ objdump -d add.o0000000000000000 <_Z3addii>:   0:	f3 0f 1e fa          	endbr64   4:	8d 04 37             	lea    (%rdi,%rsi,1),%eax   7:	c3                   	ret

整数加法被归结为八个字节。第一条指令 endbr64 是一个安全标记,不做任何计算;第二条 lea 把函数在处理器的两个寄存器(%rdi 和 %rsi)中收到的两个数相加,并把结果放进第三个寄存器(%eax);第三条 ret 返回到调用该函数的地方。名字 _Z3addii 是编译器对“带两个 int 参数的函数 add”的编码方式。

从这个例子可以得出两点观察。第一:机器码不保留名字 a 和 b,也不保留缩进和注释;这些都是为阅读源代码的人而存在的,编译器用它们来决定写出哪些指令。第二:C++ 编程者所写的内容被翻译成少数几条指令,中间没有任何媒介;这种贴近机器的特性正是 C++ 高效的原因,而正如本课程将会看到的,也是它某些危险的根源。