编辑部专板 - 谁规定的 0=1?
在初学 Python 的时候,我们会疑惑,为什么列表的第一项的索引是 0。当我们把视野拓展得更宽,会发现几乎所有编程语言的数组或列表的第一项索引都是 0,那么,究竟是谁规定的数组的第一项索引是 0
索引?偏移量!
有这样一句话:机器码生汇编,汇编生 C 语言,C 语言生万物,第一个把 0 作为第一项的索引的语言还真就是 C 语言。破案了!就是丹尼斯 · 里奇规定的 1=0。
真当如此?
C 语言是有标准的,所有的编译器都依据这个标准进行设计。在 C 任何版本语言标准,或是丹尼斯里奇自己写的《K&R C 语言程序设计》里,对于数组的设计,根本没有用索引(index)这个词语!方括号里填的,不是索引,而是Subscript偏移量!
为什么是偏移量呢?这需要从 C 语言的底层讲起了。数组的原理是开辟一片连续的内存空间,而数组的标识符就是一个存储了这片内存空间的开头的地址。这个地址也就是数组的第一项的数据的内存地址,你可以尝试这样的 C 语言代码:
#include <stdio.h>
int main(){
int arr[]={1,2,3,4,5,6};
printf("%d",*arr);//解引用指针arr
return 0;
}可见这个printf输出了数组的第一项,也就是说 arr 这个指针指向的就是第一项的内存地址。那么,使用+运算符让这个指针偏移几个内存单位呢?
#include <stdio.h>
int main(){
int arr[]={1,2,3,4,5,6};
printf("%d",*(arr+1));
return 0;
}可见,程序输出了数组 arr 的第二项,也就是索引为 1 的这一项。这样,我们就能理解为何 C 语言标准里写的是偏移量了。arr[n]访问的根本就不是这个数组的第 n 项,而是指针 arr 向后便宜 n 个内存单位后的内存地址存储的数据。那么,为什么丹尼斯里奇这样设计呢?经常写汇编的同学都知道,写汇编有多麻烦,更何况用汇编语言手搓第一个 C 语言编译器了。丹尼斯里奇大概率是想要偷个懒,为了方便就这样设计了。
为何又叫做索引了?
那么,为什么其他语言的标准定义数组的时候又把方括号里面的东西给叫做索引呢?众所周知,C 语言的数组是纯静态的,开多大就只能有多大数据,但动态数组显然应用更加广泛,动态数组就需要在开辟的内存空间最开头存储一些元数据,例如这个数组的长度,你可以参考这张表来看看你喜欢的语言都在数组头塞了多少 Metadata:
| 语言 | C++:vector | Python:list | Go:slice | Rust:Vec | Java:ArrayList | PHP |
|---|---|---|---|---|---|---|
| Metadata 长度/字节 | 24 | 40 | 24 | 24 | 32 | 144 |
显然,如果继续沿用偏移量这个概念,访问第一项就要向后偏移不同的内存单位。
而有些编程语言的动态数组实现甚至用的是链表这种内存空间根本不连续的数据结构。偏移量这种严格要求开辟的内存开头就是存储的是第一项的实现方法自然就在这些场景下当然也不能用,于是偏移量就改为索引。但这些语言的设计者又错误地沿用了 0 为第一项这个概念,规定数组的第一项索引为 0。
1=1 的语言
一些以做数据分析为主的语言,例如 R 语言,会遵循索引 1 就是数组的第一项。而做 Rolox 模组和配置文件的 Lua 语言,他的索引 1 也是数组的第一项。但零索引依然是主流。