为什么计算机从 0 开始计数?
在计算机编程中,数组和序列通常从 0 开始计数,这与人类习惯的从 1 开始计数方式不同。本文将深入探讨这一设计选择背后的历史渊源、技术原因和设计思想。
众所周知,计算机是从 0 开始计数,而不是我们平时常用的从 1 开始计数,但你有想过为什么吗?
其实不是计算机从 0 开始计数而是多数编程语言中的数组都使用 0 作为起始下标,又是为什么呢?
这个问题超纲了,程序喵不会,但是本着对科学的敬畏之心,经过大量的搜索查证,我终于找到了答案。
故事还要从一位真正的大佬艾兹格·迪科斯彻(Dijkstra)讲起,
- 艾兹格·W·迪科斯彻 Dijkstra 结构程序设计之父提出"goto有害论";提出信号量和 PV 原语;解决了"哲学家聚餐"问题;Dijkstra 最短路径算法和银行家算法的创造者;
- THE 操作系统的设计者和开发者;
- 第一个 Algol 60 编译器的设计者和实现者;
- 与 D. E. Knuth 并称为我们这个时代最伟大的计算机科学家的人。
这里贴出我翻译后的大佬语录:为了表示自然数 2,3,4…12 的子序列,一般有四种序列的表示方法:
a) 2 ≤ i < 13
b) 1 < i ≤ 12
c) 2 ≤ i ≤ 12
d) 1 < i < 13
以上的几种表达方式里,有哪一种比其他的好吗?
是的,a 和 b 有较为明显的优点:他们上下界数值之间的差值就是这个序列的长度。在任何一种表示中,两个子序列相邻,最好是其中一个的上界等于另外一个的下界,但这还不能抉择出 a 和 b 方式哪种更好,继续分析;
假设序列里要包含最小的自然数,如果使用 b 和 d 这种方式,那下界就必须是个非自然数,这就不太好看了,所以这里更倾向于使用 a 和 c 的方式,即使用 ≤ 方式表示下界。这里如果使用 ≤ 表示上界,那一个空的子序列表示方式也将会很丑陋,所以对于上界,大佬的结论是更喜欢使用 a 和 d 中的<方式,结合上一小段的分析,a 方式最终获胜,继续分析;
当需要表示一个长度为 N 的序列时,如果想通过下标来区分其中的元素,那又来了一个棘手的问题:初始元素的下标值应该用多少呢,如果从 1 开始,那范围变成 1 ≤ i < N+1,如果从 0 开始,那范围会是 0 ≤ i < N,显然后一种方式更优雅更直观,所以大佬最后的结论是自己更倾向于一个序列的表示最好从 0 开始。
在进行范围表达的时候,使用左闭右开的方式更优雅,他思考过,在处理长度为 N 的序列时,到底第一个元素的下标使用 0 更合适还是使用 1 更合适?他的出发点很简单,那就是哪种方式更优雅。首先确定使用左闭右开的方式,当下标从 1 开始时,下标范围为 1<=i<n+1,当下标为从 0 开始时,下标范围为 0<=i<n,显然后面这种方式更加优雅,所以他倾向于使用 0 作为第一个元素的下标。
难道只有优雅这一个原因吗?其实下标从 0 开始主要的意义是表示 偏移,下面举例:
数组为什么起始下标是 0?其实数组是一种线性结构,它有一段连续的内存空间,存储一组具有相同类型的数据。
如图,拿一个长度为 10 的 int 类型数组举例,系统就会为该数据分配一段连续的内存空间,空间大小为 40 个字节,其中内存块首地址 base_address = 100。

数组是可以随机访问的,当访问第i个元素时,需要定位第i个元素的地址,定位公式如下:
第i个元素地址 = base_address + i * data_type_size
其中 data_type_size 表示数组中元素类型的大小,int 类型大小是 4 字节,所以公式里 data_type_size 等于 4。在这里,下标可以理解为偏移,数组的首地址就是 base_address,其中 a[0] 就是偏移为 0 的位置,a[i] 就是偏移了 i 个 data_type_size 大小的位置,所以计算 a[i] 地址的公式为:
a[i]地址 = base_address + i * data_type_size
这里如果数组下标从 1 开始,那么 a[i] 地址的公式为:
a[i]地址=base_address + (i - 1) * data_type_size
两个公式显而易见,下标从 0 开始的更加简单,后者从 1 开始,每次访问数组元素都需要额外做一次减法操作,效率更低。
我们知道在 Python 中数组也是将 0 作为起始下标,对此 Python 之父 Guido van Rossum 也给出过正面回答,下面贴出他的翻译后的语录:
关于这个问题之前就有人在 Twitter 上询问过我,我给出过回答。这个问题我思考过很久:ABC 语言是 Python 的祖先之一,使用的索引就是从 1 开始的,而另一门对 Python 有重要影响的 C 语言,它的索引就是从 0 开始。之前的几门编程语言(Algol,Fortran, Pascal)有使用 1 作为起始索引的,有使用某个变量作为索引。而推动我使用 0 作为起始索引的原因之一就是 切片语法。
让我们先来看看切片的用例,可能关于切片最常见的用法就是"取前 n 个元素"和"取从 i 开始的后 n 个元素",如果在使用这两种用法时不需要带有 +1 或者 -1 的补偿操作,那代码会很优雅。
使用基于 0 的索引方式,那上面两种切片用法就会非常漂亮:a[:n] 和 a[i:i+n],前者是 a[0:n] 的缩写。
使用基于 1 的索引方式,如果你想用 a[:n] 表示取前 n 个元素的意思,要么使用闭合区间切片语法,要么使用起始索引加切片长度作为参数的方法。半开区间切片方法如果和基于 1 的索引方式结合起来那代码将会变得不优雅。而如果使用闭合区间切片语法的话,为了从第 i 位索引开始取 n 个元素,那就需要把表达式写成 a[i, i+n-1]。这样看来也许使用切片起始位+长度的方式在基于 1 的索引方法中更合适?这样你可以写成 a[i:n],并且 ABC 语言就是这么做的,你可以写成 a@i|n 这种特别的语法。
但是,index:length 这种方式在其它情况下也适用吗?我有点记不清了,但我认为我确实是被半开区间这种优雅的语法迷住啦。特别是当两个切片操作相邻时,第一个切片的终点索引是第二个切片的起始索引时,这种语法简直太漂亮啦。例如你想要将一个字符串使用 i 和 j 分成三部分,这三部分会是 a[:i],a[i:j] 和 a[j:],真是太漂亮啦。
这就是为什么 Python 使用 0 作为起始索引的原因。
看到这里你知道为什么很多编程语言都是从 0 开始计数了吗?
参考资料:

浙公网安备 33010602011771号