公众号旧文

为什么计算机从 0 开始,而不是从 1 开始?

作者:胖子阅读 —

在现实生活中,我们几乎所有的计数方式都是从 1 开始的。

比如第一名、第一天、第一页、第一章。

几乎没有人会说第0页、第0天。

但在计算机世界里,很多编号都是从 0 开始:

  • 第一个数组元素是 a[0]
  • 第一块网卡是 eth0
  • 第一个 CPU 核心是 cpu0
  • 程序里的循环往往从 0 开始

这看起来有些反常,但实际上,这种设计背后有着非常清晰的技术原因。

首先要理解的一点是:计算机并不是在数第几个,而是在计算偏移量。

在计算机内,数据存储在内存中,而内存本质上是一段连续的地址空间。

每一个数据都有一个具体的地址,例如:

1000
1001
1002
1003

假设一个数组存储在内存中,起始地址是 1000,每个元素占 4 个字节。

那么数组里的元素位置其实是这样计算的:

元素0 → 地址 1000
元素1 → 地址 1004
元素2 → 地址 1008
元素3 → 地址 1012

计算公式非常简单:

地址 = 起始地址 + 索引 × 元素大小

如果数组从 0 开始编号,那么这个公式就可以直接使用。

但如果数组从 1 开始编号,就必须变成:

地址 = 起始地址 + (索引 - 1) × 元素大小

这样每次访问都要多做一次计算。

在今天的计算机上,这种差别几乎感觉不到。

但在早期计算机资源非常有限时,任何额外的计算都会带来性能开销。

因此,从 0 开始编号就成为一种更加简洁、高效的设计。

除了内存计算的原因,还有一个重要的历史因素:编程语言的影响。

现代操作系统和软件的大量基础设施,都是在 C 语言的基础上构建的。

而 C 语言在设计时就规定:数组索引从 0 开始。

例如:

int a[5];

数组的五个元素实际上是:

a[0]
a[1]
a[2]
a[3]
a[4]

C 语言这样设计,正是因为它与内存地址的关系最直接。

数组名本质上就是数组第一个元素的地址,而 a[i] 实际上等价于:

*(a + i)

也就是说,数组第 i 个元素就是起始地址向后移动 i 个单位。

如果从 0 开始,这个逻辑就非常自然。

由于 Unix 操作系统是用 C 语言编写的,而 Linux 又继承了 Unix 的设计思想,因此整个操作系统生态都延续了这种从 0 开始的传统。

于是我们在 Linux 中就会看到很多类似的命名:

第一块网卡是 eth0,第二块是 eth1。

第一个 CPU 核心是 cpu0。

设备编号、数组索引、系统资源表,也几乎都是从 0 开始。

甚至在操作系统最基础的输入输出机制里,这种规则依然存在。

Linux 中每个程序启动时都会自动拥有三个文件描述符:

0 → 标准输入(stdin)
1 → 标准输出(stdout)
2 → 标准错误(stderr)

这里的 0、1、2 其实就是一个数组里的三个位置。

当然,对于普通用户来说,从 1 开始计数仍然更符合人类习惯。

所以在很多软件界面里,开发者会把内部的 0-based 编号转换成 1-based 显示。

例如程序内部可能用 0 表示第一页,但界面上会显示 第1页。

结论很明显,计算机从 0 开始,是因为这样更符合计算机的工作方式。