为什么数在计算机内存中用补码表示?
Table of Contents
负数在计算机中如何表示?
我们知道计算机是用二进制来存储数据的,举例来说,+8在计算机中表示为二进制的1000,那么-8怎么表示呢?
很容易想到,可以将一个二进制位(bit)专门规定为符号位,它等于0时就表示正数,等于1时就表示负数。比如在8位机中(用8位机做示例是为了数据短点,16/32/64位都同理),规定每个字节的最高位(最左侧那一位)为符号位。那么,+8就是00001000,而-8则是10001000。
但是,随便找一本《计算机原理》,都会告诉你,实际上,计算机内部采用2的补码(Two’s Complement)表示负数。
什么是2的补码?
先来说一下原码、反码、补码的概念:
- 原码:就是十进制数直接转成二进制数的那个码(当然如果是有符号数,则正数的最高位为0,负数的最高位为1);
- 反码:正数的反码等于原码,负数的反码就是把所有位取反(0变1,1变0),比如-8(10001000)的反码就是11110111(除最左侧符号位,其它全部取反);
- 补码:正数的补码等于原码,负数的补码=反码+1,我们把前面得到的-8的反码11110111+1(1的8位二进制为00000001),计算过程如下
11110111
00000001 +
-----------
11111000
很明显,结果就是11111000,所以,-8(10001000)的补码就是11111000。也就是说,-8在计算机(8位机)内存中就是用11111000表示的。
为什么要用2的补码?
直接用原码不是更直观吗?为什么要用2的补码呢?我们研究的时候还得转换,不麻烦吗?对于人工手算什么的来说确实麻烦,但对电路来说,不仅不麻烦,还简化了电路!因为只有这样,才能用加法电路完成减法运算,即把16-8当成16+(-8)来算。
还是以-8作为例子,假定有两种表示方法,一种是直接表示法,即10001000;另一种是2的补码表示法,即11111000,请问哪一种表示法在加法运算中更方便?
还是用前面举过例的计算式:16+(-8)=?,16的二进制表示是00010000,所以用直接表示法,加法就要写成:
00010000
10001000 +
-----------
10011000
可以看到,如果按照正常的加法规则,就会得到10011000的结果,转成十进制就是-24,显然,这是错误的答案。也就是说,正常的加法规则不适用于正数与负数的加法,因此必须制定两套运算规则,一套用于正数加正数,还有一套用于正数加负数,从电路上说,就是必须为加法运算做两种电路,或者也可以认为一套加法电路,一套减法电路。
现在,再来看2的补码表示法
00010000
11111000 +
-----------
100001000
可以看到,按照正常的加法规则,得到的结果是100001000。注意,这是一个9位的二进制数。我们已经假定这是一台8位机,因此最高的第9位是一个溢出位,电脑根本就存不下,所以那个位就相当于被“丢掉了”。所以,结果就变成了00001000,转成十进制正好是8,也就是16 + (-8)的正确答案。这说明了,2的补码表示法可以将加法运算规则,扩展到整个整数集(即适用于正整数和负整数),从而用一套电路就可以实现全部整数的加法。
2的补码的本质及正确性
“模”的概念
“模”是指一个计量系统的计数范围,如时钟等。计算机也可以看成一个计量机器,它也有一个计量范围,即都存在一个“模”。例如:时钟的计量范围是0~11,模=12,n位计算机计量范围是0~2n-1,模=2n(比如8位计算机,28=256,所以它的模就是256,它能表示的数值范围是0~28-1=0~255)。
“模”实质上是计量器产生“溢出”的量,它的值在计量器上表示不出来,计量器上只能表示出模的余数。任何有模的计量器,均可化减法为加法运算。
例如:假设当前时针指向10点,而准确时间是6点,调整时间可有以下两种拨法:你可以往回拨4个小时,也可以向前拨8个小时(10+8-12=6,在钟表系统里模是12),在以12模的系统中,加8和减4效果是一样的,因此凡是减4运算,都可以用加8来代替。
对12这个“模”而言,8和4互为补数。实际上以12模的系统中,11和1,10和2,9和3,7和5,6和6都有这个特性,共同的特点是两者相加等于模。
对于计算机,其概念和方法完全一样。对于n位计算机,设n=8,所能表示的最大数是11111111(8个1),若再加1则变为100000000(9位),但因只有8位,最高位1自然丢失。又回了00000000(8个0),所以8位二进制系统的模为28(即256,相当于一个时钟有256个刻度,当走到255的时候,再加1就又变成0(256)了),我们把时钟的12、256也说成0,是因为它是原点,在数学上原点一般用0表示。在这样的系统中减法问题也可以化成加法问题,只需把减数用相应的补数表示就可以了。
2的补码等于反码+1的原因
再次重申一下这句话:在以12为模的系统中,加8和减4效果是一样的,因此凡是减4运算,都可以用加8来代替。
所以对于模为100000000(256)的8位系统来说,减去b和加上100000000(8个0)再减b是一个道理,而(100000000-b)是什么?恰好就是b的补码(因为模减去一个数得到的数就是它的补码,就和12-4=8,8就是4的补码一样),而100000000(8个0)是什么?其实它就是11111111+1,即100000000-b=11111111+1-b=(11111111-b)+1,好了,为什么我要把(11111111-b)括起来呢?因为(11111111-b)就是b的反码。
我们来验证一下反码的求法,我们就假设b=4,4的8位二进制码为00000100
11111111
00000100 -
-----------
11111011
可以看一下,负数的反码就是把1变0,0变1,而11111111因为全是1,所以被减的数为1的位就被减掉了就会变成0(即1变0),而为0的位就相当于没减,相当于0变1,所以11111111减去某个数,就相当于对这个数取反,这就是2进制数的补码=反码+1的原因。
为什么int范围是-2147483648~2147483647
一般对于x86架构的电脑/服务器来说,int型是32位(虽然现在都是64位机,但int还是32位,long int才是64位),当然其它架构中(比如单片机),可能是16位。
32位二进制数总共可以表示多少个数?
32位二进制数可以表示几个数,意思就是32个位置,每个位置有0和1两种可能,一共有几种组合方式?答案是232=4294967296个,这是个数学问题。我们先拿两位来说,第1位有0和1两种值,第二位也是有0和1两种值,那两位加起来一共能有几种组合?可不就是2×2=22=4种组合嘛,同理,如果有3位,那就是2×2×2=23=8,……,如果有32,那就是2×2×2×……×2(32个2)=232=4294967296种组合。
每一种组合表示一个数,比如0……00(32个0)表示0,0……01(31个0)表示1,……,1……11(32个1)表示4294967295,32个1已经是它能表示的最大数了,即32位二进制数能表示0~表示4294967295共4294967296个数。
有人可能会有疑问,232表示的最大数明明是4294967295,为什么我却说它能表示4294967296个数?因为是0开头呀,算上0就是4294967296个了。比如0~10,虽然最大数是10,但它有11个数,因为它是0开头,如果1开头才是10个数。
32位有符号二进制数
对于一个有符号整型来说,它的最高位(最左边那一位用来表示正负,0表示正,1表示负),所以32位二进制能表示的最大的有符号整型数是111……1(31个1),但是31个1不好换算为十进制,我们一般把它加1,这样它就变成了1000……0(31个0),这个数的大小是231=也就是2147483648,因为加了1,我们要减去1,也就是2147483648-1=2147483647。
当符号位为0时,这个数自然就是+2147483647,当符号位为1时,这个数就是-2147483647,所以正数的范围是+1~+2147483647(共2147483647个正数),负数的范围是-1~-2147483647(共2147483647个负数),除此之外还有0没算进去,这样加起来,32位二进制表示的数一共有2147483647×2+1=4294967295个数。
有没有发现,正数的数量(2147483647个)加上负数的数量(2147483647个)再加上0(1个),总共才4294967295个数,可是前面明明算出来,32位进制数是可以表示4294967296个数的,现在我们算出来的却是4294967295个,为什么会少了一个数?少的那个数是几?它藏哪儿去了?
这个问题,看下图就明白了,这个图是按16位二进制数画的,因为32位的数太长了,不好写,但原理是一样的
想像这个圆是一个巨大的时钟,我在它上面均匀的刻了216=65536个刻度,左半边有-1~-32767共32767个数,右边有1~32767共32767个数,左右两边加起来共32767×2=65534个数,很明显,除了左半边和右半边,我们还少算了中间两个数,一个+0,一个-0,加上这两个数就刚好是65536个(216),可是0其实并没有正负的,0就是0,不管+0还是-0都是0,这怎么处理?
计算机中每一个数字都是一个不同的0和1的组合,我们已经知道,16位二进制数,每一位有0和1两种值,这16位的0和1,一共可以组合出216=65536种组合,每一种组合都表示一个数字,当我们把最高位作为符号位时,则只有15位参与组合,15位一共可以组合出215=32768种组合(0~32767),再加上符号位有0和1两种情况,所以总共有两个32768种组合,也就是32768×2=65536种组合。
然而,这65536种组合里,有两个组合分别是这样的:
0000000000000000 (16个0)
1000000000000000 (15个0)
经过前面的知识,我们很清楚,第一个就是我们前面所说的+0,第二个就是我们所说的-0,然而内存中存的是补码,也就是我们看到的这两个数,其实是补码来的,而要算出它的原码,正数就不用说了,原码与补码相同,而负数的补码=反码+1,则反码=补码-1,算出了反码再按位取反就能得到原码。
下面我们来算一下1000000000000000(15个0)的反码是多少,其实很简单,不用算,减1就是一直向高位借位,直到符号位才能借到(注意符号位是参与运算的,这就是为什么可以向符号位借位的原因),结果就是0111111111111111(15个1)。
反码求出来了,除了最高位(最左侧)的符号位0外,其它全部按位取反,就变成了0000000000000000(16个0)。你发现没?1000000000000000(15个0)这个补码,对应的原码是0000000000000000,而0000000000000000(+0)这个补码对应的原码也是0000000000000000,也就是说,现在出现了一个“bug”,有两个补码,它的原码竟然都是0000000000000000。对于0000000000000000这个补码,我们已经用它表示0了,但1000000000000000这个补码呢?它实际上表示的也是0,但是0只有一个,我们不能重复表示呀,怎么办?
我们试试直接把1000000000000000(15个0)这个数转换为十进制,结果是215=32768(此时已经不把最高位当符号了,因为你把它当符号位的话,根本就不用算,因为它直接就是0了,跟正0重复了),但是这个数理论上确实是负数,因为它的符号位是1,于是就人为规定,它是-32768(即1000000000000000这个补码对应的十进制数为-32768)。
为什么不把它规定为+32768呢?很简单,首先它本身就应该是负数(因为它符号位是1)。其次,一般我们把0归到正数,即正数是0-32767(共32768个数),这时负数就是-1~-32768(刚好也是32768个数),这样正数负数刚好对半分,而如果归到+32768,则必须把0给负数那边,这样才能刚好正负数各一半,这样有点不太符合人们的习惯。
前面说的都是16位的,同理,如果换成32位二进制,那么它的左半边肯定是-1~-2147483647(共2147483647个数),右边是+1~+2147483647(共2147483647个数),两边加起来共2147483647×2=4294967294,很明显我们少算了+0和-0,加上这两个数,刚好是4294967296个数,刚好就是32位二进制数能表示的数的个数,这里也是一样,+0和-0的补码都对应整数0,所以我们就让+0为0,并人为规定-0是-2147483648。