传统上,计算机计算从零开始的数值。例如,基于C的编程语言中的数组从索引零开始。
造成这种情况的历史原因是什么?从零开始计数比从一开始计数有什么实际优势?
注意:此问题要求提供解释清楚的技术答案,而不仅仅是观点,并且旨在涵盖一般计算机,而不仅仅是编程。此问题扩展到程序员问题“为什么结构/数组从零开始?” 。
传统上,计算机计算从零开始的数值。例如,基于C的编程语言中的数组从索引零开始。
造成这种情况的历史原因是什么?从零开始计数比从一开始计数有什么实际优势?
注意:此问题要求提供解释清楚的技术答案,而不仅仅是观点,并且旨在涵盖一般计算机,而不仅仅是编程。此问题扩展到程序员问题“为什么结构/数组从零开始?” 。
Answers:
从0开始计数数组简化了每个元素的内存地址的计算。
如果将数组存储在内存中的给定位置(称为地址),则每个元素的位置可以计算为
element(n) = address + n * size_of_the_element
如果您将第一个元素视为第一个元素,则计算将变为
element(n) = address + (n-1) * size_of_the_element
差别不大,但是每次访问都会添加不必要的减法。
编辑
address + n * size_of_element
只要“地址”是第零个元素的地址,就可以始终计算每个元素的位置。无论第零个元素是否作为数组的元素存在,这都可以正常工作。问题是为什么第零个元素存在,而不是为什么我们将地址存储为(可能是名义上的)第零个元素的地址。(这将回答。)
v[n]
,则必须计算表达式的地址。如果索引以0开头,则计算为v + x * size。如果为1,则计算为v +(x-1)* size。例如,v [1]对应于v +(1-1)*大小,即
*array
实际上是指第一个元素,这一点很重要。一个例子:如果我们array
指向第一个元素之前的存储位置,则强制转换为其他类型的数组会很麻烦。int
s 数组中第二个字节的位置将取决于字长;在32位计算机上,它将是((char*)intArray + 5)
!!
尽管以下原理适用于十进制以及任何其他基数,但从表示计算机中使用的数字的固定数字二进制系统中自然可以很容易地理解计算机中从0开始计数。如果您有8位,则可以表示256个1和0的可能组合。您可以使用这些8位来表示数字1-256,但这会省略0,这在数学中本身就是一个数字,因此在数学中很有用,因此它们用于表示数字0-255。
这已经设置了自然顺序的先例,从0(二进制表示中的全0)到255(8位数字中的全1)开始。考虑到代表数字的系统,从0开始是有意义的,因为0是系统中的“第一个”数字,所以1是“第二个”数字,依此类推。
在计算机中从0开始如此方便的另一个原因是偏移量的概念。偏移量是一个数字,表示与内存或硬盘或任何其他“可寻址”介质中某个位置的距离。在计算机中,实际上所有数据都是线性存储的,这意味着该数据有一个顺序,第一个字节,第二个字节等。通过偏移量表示数据“区域”的位置很方便。数据块中的第一个字节是什么?它在偏移量“ 0”处,这意味着在数据块中第一个字节之后的0个字节处可以找到它。尽管可以将“ 1”指定为第一个字节,但是由于以下几个原因,这会在数据表示中带来麻烦:
从来没有想过超级用户会给像我这样的扶手椅哲学家带来机会。这里有一个基本的误解,因为非哲学家倾向于跳过一些细节。简而言之: 计算机不是从零开始计数,而是仓位面额从零开始。
对于计算机和人类(任何)计数技术之间的这种不一致,没有任何混淆。让我们分解问题。
为什么计算机从零开始计数?
计算机计算从零开始的值。例如,C语言中的数组。
零实际上是表示某物或标尺的中点的空白。对任何事物进行计数都是不实际的,因为根据零的定义它是不可能的。
在与刻度的中点相同的意义上,零可以用来表示集合的最边缘(绝对起点)。这个问题毫无意义,因为它在“计数值”和“从零计数”之间是不一致的。
因此,是的,计算机的计数从零开始,但从一开始计数。这两个词具有不同的含义。
名词
动词(与宾语一起使用)
(dictionary.com)
Dougvj已充分描述了实际原因,在此我无话可说。如果我们能请一位60年代的CS教授讲一个历史的话...
我觉得这已经由之前“覆盖prof.dr.艾兹赫尔·戴克斯特拉 ” - 巴勒斯研究员日期为1982年8月11日的一封信:CF EWD831
标题:为什么编号应从零开始。“有理由比另一项更喜欢一个公约吗?是的,有...。”
还请注意,Dijkstra 一直待到1968年才加入ALGOL 68设计团队。Algol68允许使用0、1或程序员认为适合该算法的任何数字组成的数组。cf(“ Algol 68的制作”讲述了““您能定义三角形阵列吗?”有人(托尼·霍亚尔)打断。“不仅是三角形,甚至是椭圆形的” Aad回答,并展示了如何。
具体来说,在Algol68中,对数组(和矩阵)进行切片时,它们的索引为@ 1,因此对[1:...]数组有偏差。但是,可以通过指定“ @ 0”(例如向量x [4:99 @ 2],矩阵y [4:99 @ 1,4:99],将“ 1st ”的下界移动到从“ 0th ”位置开始@ 0]。类似地有一个默认/偏压从在1 DO〜OD环(除非“ 从 0”明确地陈述),并从1为整数的情况下我在〜,〜,〜ESAC和$ C(〜,〜,〜 )$ 选择条款。
似乎Dijkstra对1968年3月的报告草稿(MR93)的评论和他的坚持激起了可以说是使用前的网络火焰战争:“尽管有不合语法的地方还是有吸引力的,还有其他的文章都非常符合语法要求。我实在无法向肤浅的人解释。” EWD230
Algol 68最终报告(FR)于1968年12月20日发布,当时该报告在慕尼黑会议上遭到反感,然后被工作组通过。随后被联合国教科文组织批准的大会报告IFIP出版。
大约在1968年12月23日(?),Dijkstra,Duncan,Garwick,Hoare,Randell,Seegmuller,Turski,Woodger和Garwick签署了AB31.1.1.1“少数民族报告”,第7页(1970年出版)。
别人提出的距离比喻很实用:
“你家离最近的加油站有多远?”
“ 1英里。”
“你住在加油站吗?”
“不,如果我住在加油站,那将是0英里”
“为什么从零开始而不是从一开始计数?”
另一个很好的例子是生日-我们不是说某人的生日是一岁,而是一年之后。
我们说leap年或美国总统选举是每四年一次,即使您从以下年份算起也是如此:2000年,2001年,2002年,2003年,2004年是五年。(顺便说一句,罗马人确实把它搞砸了一段时间,并且leap年距离太近了)
我的观点是,在现实世界中,我们始终从零开始“计数”-“在[数组的开始]之后有多少个位置是您想要的元素”,恰好是您要从零开始计数的问题在许多计算机程序中。您不会说第一个元素是起点之后的一个位置,是吗?这是开始。
正如其他人已经说过的,计算机不从零开始计数。
某些语言从0开始索引。从0开始索引有两个主要优点:
它可以自然地转换为汇编,因为它可以解释为从指针到第一个位置的偏移量。
当你想要负面的时候,你不会变得很奇怪。从1BC到1AD多少年?没有。因为尽管BC实际上是负日期,但没有零年。如果存在0AD,那么这里就不会有任何问题。您会在整个科学领域中看到同样的问题,因为人们天真地将集合中的第一个元素定义为+1。
这是计算篮子中苹果的算法:
count := 0
for each apple in basket
count := count + 1
执行上述操作后,count
保存苹果的数量。它可能为零,因为购物篮可以是空的。
如果您整个月都没有使用信用卡,您会收到1美元的账单吗?还是1美分?
当您重置汽车里程表上的行程表时,它会变为0001还是0000?
考虑由32位结构组成的数组,d
每个结构由16位字组成w
。每个字由两个8位字节组成b
。在零索引下,覆盖看起来非常方便:
d: | 0 | 1 |
w: | 0 | 1 | 2 | 3 |
b: |0|1|2|3|4|5|6|7|
d[1]
字地址处的32位对象w[2]
很容易通过将索引乘以2来计算,该值是32位和16位对象的大小之比。此外,在字节寻址中为b[4]
。
之所以可行,是因为在每个度量单位中,零为零:字节,字,双字等。
看上图:它看起来很像标尺,单位转换很直观。
使用基于一个的索引,它会中断:
d: | 1 | 2 |
w: | 1 | 2 | 3 | 4 |
b: |1|2|3|4|5|6|7|8|
现在我们不能简单地将d
索引乘以2来获得w
索引,也不能简单地乘以4来获得b
索引。单位之间的转换变得笨拙。例如,从d[2]
到b[4]
,我们必须计算((2 - 1) * 4) + 1 = 5
。
我们必须减去讨厌1的d
单位偏差,然后在基于自然零位的坐标系中进行缩放,然后再将讨厌1的b
单位相加。请注意,它不一样1!我们减去一个双字宽度,然后加一个字节宽度。
在数据的不同视图之间进行转换就像是摄氏温度转换。
那些说基于一的数组在实现级别很容易处理的人,因为只是简单地减去1就骗了自己,而你却在自欺欺人。仅当我们不对不同数据类型进行任何缩放计算时,这才是正确的。这种计算发生在对数据具有灵活视图的任何程序(例如,也可以作为一维数组访问的多维数组)或操纵存储的程序中:例如,内存分配器,文件系统或视频帧缓冲库。
在任何基数中,如果要使用最少的数字来实现作为基数幂的值范围,则必须从零开始。例如,以10为底的三位数足以为我们提供从0到999的一千个不同值。如果我们从1开始,我们只会溢出一个值,因此我们需要四位数。
这在计算机中很重要,因为二进制中的位数转换为硬件地址线。例如,其中有256个字的ROM芯片可以从0到255寻址,这需要8位:00000000到11111111。如果从1到256寻址,则需要9位。我们必须浪费更多的地址走线到电路板或集成电路。所以在实践中可能会发生的是,将只调用 0在用于访问该芯片的软件API级别上为1。对字1的请求实际上会将00000000放在8位地址总线上。否则,对1的请求将按预期转换为地址00000001,但对256的请求将映射到其他情况下未使用的8位地址00000000而不是9位地址100000000 。搜索问题,并且在硬件,软件以及所有用户界面和文档中始终使用0到255完全可以避免。
以西方音乐理论为例。我们有七个音符的全音阶音阶,但我们称它们为八度音阶的空间!然后间隔的倒数遵循九的规则:例如,三分之一的倒数是第六(从九中减去三)。所以三个简单的数字在起作用:七个(音阶),八个(八度音阶)和九个(减至倒数)。
如果七个音符构成一个七音阶或七音阶,并且音程是从零开始的,那么我们将从七个音符中减去以求倒数。一切都基于七个。
此外,间隔很容易堆叠。在当前系统中,如果我们先跃升五分之一,然后再跃升四分之一,然后再跃升三分之一,我们就不能仅仅添加这些。结果间隔减少了两个。它不是十二分之一,而是十分之一!在每个阶段,我们都必须减去一个。上升五分之一然后上升四分之一不是九分之一,而是八度。
在合理设计的音乐系统中,我们可以添加间隔来确定最终的跳跃。在同一音符上开始和结束的一系列音符将具有类似于电路周围的电压定律的属性:所有间隔将加为零。
音乐理论和写作已经过时了。自从用蜡烛点燃羽毛笔作曲以来,大部分内容都没有改变。
当2000年到来时,许多人感到困惑,为什么新千年还没有开始。那些指出直到2001年才开始的人被认为是党的oper子和矮人。毕竟,您20岁时就已经20岁了,对吧?不是21岁时。如果您认为千年始于2000年1月1日,那么您无权抱怨任何编程语言中从零开始的数组。他们以您喜欢的方式工作。(但是,是的,一维位移和阵营的拥护者是矮人和共产党人。百年应该在XX00年开始,而千年应该在X000年开始。)
手表上的每一分钟都是从:00秒开始的。每个新的小时从00:00分钟和秒开始。而且,至少在24小时制时,午夜来临的时候轮到一天了,并且11:59:59递增到00:00:00。
因此,如果您想从午夜开始计算13:53:04之类的秒数,则只需计算13 * 3600 + 53 * 60 + 4
。没有平淡的1
加法或减法。
好的,对于音乐家,甚至是技术熟练的音乐家来说,这是什么?
MIDI!它在消息的实际有线表示中对程序和通道使用从零开始的编号,但是齿轮将其显示为从1开始!例如,大多数齿轮上的0到127程序被称为1到128,但是有些人将其称为0到127,甚至给用户一个选择。
程序71到80被认为是十个的“库”。例如,它在我的MIDI踏板上说得很对。脚踏开关标记为1到10,如果我在第七银行,它们会选择程序71到80。但是,某些设备或计算机软件会将1-128程序号显示为0到127,甚至给用户一个选择!更糟的是:基于1的系统,或同时使用基于1和0的情况造成的混乱?
MIDI通道号称为1到16,但以0到15二进制表示。似乎尽管出于基于一的表示,但某些设备还是使用了一个Dispswitch来配置通道号,并且通常,这些开关仅使用基于零的二进制代码。因此,如果要使用通道3,则必须将其切换为0010(二进制2)。
如果我从编程语言概念课上正确记起...语言被索引为0,其他语言被索引为1,则与历史原因有关。实际上,编程语言的祖父Algol-68,Fortran和其他一些“业务”语言(例如COBOL)都被编入了索引。但是,在其中一些语言中,您实际上可以明确指定起始索引是什么。有这样一个有趣的表在这里。
基本上回到“ Ye Olde Days ”,数学家,科学家和其他“学术界”通常使用0索引语言,而使用COBOL之类的语言的用户发现从0开始计数是没有用的,因此在这些语言中更有意义从1开始(似乎不太混乱)。
现在,如果您的问题是关于为什么一台计算机(不是一种语言)自然会从零开始计数的原因……那么我猜想二进制实际上是固有的:ex:
0000
= 0
0001
= one ...依此类推向前...
在描述与某物的距离时,从零开始是可行的。因此在此数组中:
[4,9,25,49]
从数组开始到25的距离为2-您需要跳过两个步骤才能到达那里。到4的距离为零-完全不需要从头开始。
累加距离(或索引)时,像这样思考是可行的-我先走一步,然后零步,再走两步,我在哪里?我的索引是1 + 0 + 2 =3。跳过三个步骤,我最终在上面的数组中为49。
记住数字是如何在计算机中表示的。让我们来一个byte
变量。0 以二进制表示为00000000 1。1是00000001。2是00000010。依此类推。
请注意,a byte
可以存储的最低数字为0。如果我们从1开始数组索引,则系统效率不高,因为我们现在拥有长度为255而不是256的数组。由于C程序中的数字会编译为二进制数(int
通常是unsigned int
s,通常是数组索引中的s),将0用作起始索引似乎很自然,因为它效率更高。
此外,在C ++中,a[p]
展开为*(a+p*n)
,其中n
数据类型的大小。换句话说,a[p]
意思是“给我索引中的元素a+n*p
”。如果以p
开始1
,则index处将有一个空白/未使用的部分a
。
1.当然,出现了一个明显的问题“为什么”。为什么不将00000000设置为1?简单:二进制加法(通过全部加法器单元的级联完成)在00000000为0时在硬件中很容易。二进制加法是所有算术运算必不可少的部分。如果使它表示为1,则可能需要告诉编译器从所有数字中减去1,或者需要对加法器电路进行硬连线,以从加数中首先减去一个,然后再将其加回到总和上。(请注意,您以后不能再减去一个,因为可能涉及进位)
现有的好答案尚未提及的一件事:基于零的索引与模运算一起很好地工作,因此可以将其组合以形成循环列表。例如考虑类似
color = colors[i % colors.length]
这可能会为每个对象(由索引i
)赋予与列表不同的颜色colors
,直到使用完所有颜色为止,这时它将从头开始。在基于索引的索引中表达相同的内容非常笨拙:
color = colors[(i - 1) % colors.length + 1]
固定大小的无符号二进制算术具有环绕功能所施加的自动模运算是为什么这样做有意义的另一个示例。
要考虑的另一件事是,不使用基于零的数组的第一个元素非常容易。(这不适用于foreach
将数组作为一个整体对待的-style迭代和类似的语言构造。)许多程序员(包括我本人)对浪费的空间可能会感到有些尴尬,但是在大多数情况下,数量如此之小,以致于这些后顾之忧是没有根据的。另一方面,如果语言正在使用基于一个的索引,那么没有大量代码就无法模拟索引为零的元素。因此,鉴于在某些情况下,从零开始的索引比基于一的索引更好,所以在任何地方都选择零作为索引 与基于各处的基于位置的方法不同,它是一种更灵活的方法,并且比可配置的起始位置更加一致。
计算机系统同时使用自然数(从0开始计数)和整数(从1开始计数)。人们用整数对事物进行计数,这使它们对于编号列表很直观,许多编程语言都利用了这一优势:BASIC,COBOL,Fortran,Lua和Pascal都从1开始计数。这些语言的目标是数据处理,数值分析,和教学,其中简单,直观的列表是一个优势。
当您开始分析和处理数据结构时,整数会变得笨拙,而不仅仅是按顺序处理所有数据。当您需要在公式或算法中引用序列时,将它们从0进行编号更容易且更容易出错,就像数学家所做的那样:a 0,a 1,n等,否则,您通常必须调整+1和–1来获取正确的数据,并且很容易出错,从而产生错误。因此,为计算机科学家设计的语言通常使用自然数:C,Java和Lisp都从0开始计数。
除编程语言外,许多计算机系统都从0开始编号,因为这是计算机科学家习惯的。同样,因为从1开始编号会导致许多隐患,所以我们中许多人在严格为非技术最终用户设计的界面元素之外避免使用它。
简单的答案是第一个数字不是1,而是0。
说明:用于计算任何基数的多位数的公式为:
n = sum(i=0 to n, Di^i)
WHERE
n = numeric result
i = index (starting with 0)
Di = is the digit at index i
让我们以十进制表示,这是我们最常用的十进制。
查看数字1234,我们可以将其写为:
4 x 10^0 = 4
3 x 10^1 = 30
2 x 10^2 = 200
1 x 10^3 = 1000
in other words, sum of digits raised to the power if their index.
因此,不仅仅是计算机,我们人类也从零开始计数。
除了计算效率外,还有另一个方面需要计数。有两种方法可以为序列中的每个元素赋予序号:
人们的年龄是基数:婴儿出生后的第一年为0岁,因为它已经存在了整整零年。
日期中的年份是序数:在第一年Anno Domini(AD)中,年份是1 AD。没有零年,就像没有零。
编程语言(例如Matlab和Mathematica),其中元素的索引代表其在数组中的位置,从1开始,即第一个元素开始计数。在其他语言(例如所有基于C的语言)中,元素的索引是前面元素的数量,因此第一个元素是0。
当然,当指出基于零的索引更有效时,Matteo只是部分正确。
element(n) = address + n * element_size
如果所有数组地址都已element_size
减去一个,则基于1的索引可以同样有效。这可以在分配数组时完成,在这种情况下,速度一样快:
array_address = address - element_size
element(n) = array_address + n * element_size