公众号旧文
为什么计算机从 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 开始,是因为这样更符合计算机的工作方式。