本文源自汇编课上的一个问题:8位二进制数的原码能表示什么范围的数?大家知道8位二进制数应该可以表示
2^8=256
个数,可是原码的第1位表示正负,不算0 剩下7位可以表示2^7-1=127
个数,再算上0,一共是2*(2^7-1)+1=255
个数,所以到底漏了谁?
我们知道计算机最初就是为了完成计算而设计制造的,那么数字1在计算机中是怎样表示的呢?
二进制
由于电路最容易表示的是高低电平(高: 1
;低: 0
),而我们知道数在不同进制下虽然表示不同,但是可以一一对应,即进制之间是相互等价的。所以自然,二进制成了不二之选。
但是负数又该怎么办呢?
负数
数字的正负只是记号,可以用0、1表示。于是,原码诞生了。
数字变为二进制编码的原初转换即为原码。转换规则如下:
-
非负数
直接转换为二进制
-
负数
先将绝对值转换为二进制,再将最高位写1,表示负数
以8位(8 bit)二进制数为例, 10
的原码是 00001010
, -3
的原码是 10000011
。那么一个很自然的问题是:8位二进制数能表示什么范围的有符号数2呢?
如果不考虑负数,我们知道8位二进制数可以表示 0~255
( 2^8=256
个数);考虑负数后,第1位表示正负是两种可能,后面7位去除 0000000
的情况是 2^7-1=127
种可能(保证严格正负,即这些情况是不重复的),最后还剩 00000000
、 10000000
这两种情况( 2*(2^7-1)+2=2^8
),那这两个数对应的十进制数究竟是什么呢?除去这两个数,现在已经表示的数是 -1~-127
和 1~127
,0还没有表示,哪个表示0呢,另一个又表示哪个数字呢?我们不妨将这些确定的数先简单列出来。
十进制 | 二进制 | | | 十进制 | 二进制 |
---|---|---|---|---|
1 | 00000001 | | | -1 | 10000001 |
2 | 00000010 | | | -2 | 10000010 |
3 | 00000011 | | | -3 | 10000011 |
… | … | | | … | … |
126 | 01111110 | | | -126 | 11111110 |
127 | 01111111 | | | -127 | 11111111 |
其实根据先前的定义,0的原码就是 00000000
,可为什么会有这个定义呢? 10000000
表示的又是哪个数字?此外,虽然可以使用上面的原码表示负数,但是方便计算机进行运算吗?我们知道计算机最擅长的是做加法运算,如果负数以一种恰当的方式被表示,那么 a-b
就可以看作 a+(-b)
,从而将减法转换为加法。原码可以做到吗?
观察上表就会发现负数的二进制表示随着数的不断变小,反而在增加,如 -1>-127
,而 10000001<11111111
,这样是不可能将减法转换为加法的(正负求和规律与正正求和、负负求和不一致),例如 1+(-1)
与 2+(-2)
对应的二进制相加是 00000001+10000001
和 00000010+10000010
,结果分别为 10000010
和 10000100
,显然这两个二进制结果不相等,而从十进制看结果应该均为0,矛盾。所以原码实际上并没有解决负数在计算机中的表示问题。
不过,它的思路是正确的,可以用0、1表示正负。
补码
在引入补码前,我们先看一个相关的例子。
现在有一个时钟(1点到12点),如果认为12点是0点,那么3点就是 +3
,表示时间向后过了3个小时,11点就是 +11
,表示时间向后过了11个小时,那 -2
是不是表示时间向前了2小时呢,所以可以认为 -2
表示的是10点吗?
时钟 | 数字 | 数字 |
---|---|---|
12点 | 0 | 0 |
1点 | 1 | -11 |
2点 | 2 | -10 |
… | … | … |
10点 | 10 | -2 |
11点 | 11 | -1 |
所以某种程度上 -2=10
?
原因是时钟以 12
为周期,一个数放在这周期里就相当于 mod 12
(除以12取余数),变成了余数。而 -2
与 10
在 mod 12
的情况下,表示的含义是一样的。再进一步,可以认为 12
就是时钟能表示的最大信息量。
那么,计算机中的负数是否可以按照同样的思路表达?
例如,在8位二进制下,如何表示 -1
?
首先,8位二进制,相当于 mod 2^8=256
(加上 256
或者减去 256
对这个数没有影响,因为 256
超过可表达的范围无法记录),所以我们给 -1
加上 256
试试,则 -1+256=255
的二进制将表示 -1
。 255
本身不在有符号数的表示范围内,所以不会产生误解。如果我们继续下去并考虑正数的情况(正数直接转二进制),可以得到这样一张十进制与二进制映射表。
十进制 | 二进制 | 十进制 | 二进制 | |
---|---|---|---|---|
1 | 00000001 | -127 | 10000001 | |
2 | 00000010 | -126 | 10000010 | |
3 | 00000011 | -125 | 10000011 | |
… | … | … | … | … |
126 | 01111110 | -2 | 11111110 | |
127 | 01111111 | -1 | 11111111 |
从 -127
开始到 -1
,再从 1
到 127
,数字不断变大;再看对应的二进制, -127
( 10000001
)到 -1
( 11111111
)也是不断变大, 1
( 00000001
)到 127
( 01111111
)也是不断变大;可是 -1
( 11111111
)到 1
( 00000001
)似乎变小了。没关系,我们先来确定 -1
和 1
之间的 0
的二进制表示是什么。如果我们给 1
( 00000001
)减1,得到 0
的二进制表示是 00000000
;给 -1
( 11111111
)加1,得到 0
( 100000000
),舍弃第9位的1后变为 00000000
(8位二进制只能记录8位数字)。所以可以确定 0
的二进制就是 00000000
,而不是 10000000
,那么 10000000
是哪个数的二进制呢?
从上表看就很容易知道了, -1
( 11111111
)到 -127
( 10000001
)在不断减小,继续减小 1
是 -128
( 10000000
),此时 2^8=256
个8位二进制全部对应上了十进制数。这样的二进制,我们称为补码。

上图清晰地表示了十进制与对应的补码的映射关系,所以明显地,8位二进制数能表示的有符号数(要求对称的情况下)是 -128~127
。理论上,我们其实可以给每个数加上或减去一个数做偏移,使得8位二进制数能表示的有符号数是 -127~128
、 -60~195
或者 -1~254
等,但是显然没有补码的表示方法好(非负直接转换为二进制;负数加 256
再转换为二进制)
比较原码和补码,回头看原码之所以不能作为计算机表示负数的方法,正是它的二进制表示不是递增加一(原本的十进制数 -127
到 127
是递增加一的),所以转换后不满足加法运算规律;而补码是满足递增加一的。
算法
前面补码的计算过程中有一步是给原数(负值)加上对应位数的一个数(如 8 bit就是 2^8
),这里存在一个严重的问题。我们计算补码就是为了表示负数,将减法运算转换为加法运算,但是依据上面的算法,计算补码的过程中就需要做减法运算,所以
那有没有其他算法可以将一个十进制数转换为补码呢?
十进制 | 原码 | 补码 |
---|---|---|
-1 | 10000001 | 11111111 |
-2 | 10000010 | 11111110 |
-3 | 10000011 | 11111101 |
… | … | … |
-126 | 11111110 | 10000010 |
-127 | 11111111 | 10000001 |
由于计算十进制数的原码是不难的,负数只需要先将绝对值转换为二进制,再将最高位写1,所以列出 -1
到 -127
的原码和补码,试图找到原码转换为补码的方法。经过观察不难发现,原码和补码就像是颠倒了一样,一个从 10000001
到 11111111
,另一个从 11111111
到 10000001
;还有就是原码和补码的最高位均为1。所以很自然会去尝试在保证最高位不变的情况下,将其余位做变反处理( 0
变 1
; 1
变 0
),而这就是反码(负数的反码;正数的反码和原码相同)。
十进制 | 原码 | 反码 | 补码 |
---|---|---|---|
-1 | 10000001 | 11111110 | 11111111 |
-2 | 10000010 | 11111101 | 11111110 |
-3 | 10000011 | 11111100 | 11111101 |
… | … | … | … |
-126 | 11111110 | 10000001 | 10000010 |
-127 | 11111111 | 10000000 | 10000001 |
观察上表不难发现,反码加1就是补码。至此,已经找到计算负数的补码的方法了。可是还有一个问题我们还没有解决:原码为 10000000
的十进制数是多少?
让我们倒着推导一下! -128
的补码是 10000000
,反码等于补码减1,是 11111111
(借位,且负数的原、反、补第一位都是1;非负数的第一位都是0),原码是反码除第一位全部变反,是 10000000
!所以原码为 10000000
的十进制数是 -128
!
总结
十进制 | 原码 | 反码 | 补码 |
---|---|---|---|
127 | 01111111 | 01111111 | 01111111 |
126 | 01111110 | 01111110 | 01111110 |
… | … | … | … |
3 | 00000011 | 00000011 | 00000011 |
2 | 00000010 | 00000010 | 00000010 |
1 | 00000001 | 00000001 | 00000001 |
0 | 00000000 | 00000000 | 00000000 |
-1 | 10000001 | 11111110 | 11111111 |
-2 | 10000010 | 11111101 | 11111110 |
-3 | 10000011 | 11111100 | 11111101 |
… | … | … | … |
-126 | 11111110 | 10000001 | 10000010 |
-127 | 11111111 | 10000000 | 10000001 |
-128 | 10000000 | 11111111 | 10000000 |
原码、反码、补码的引入是为了解决计算机中使用二进制表示负数的问题,先是从0、1表示正负的思路开始,到通过加除数( mod
)计算补码,最后是加入了反码找到一种计算补码的通用算法。由此,也就容易理解为什么非负数的原码、反码、补码都相同,都是直接转换为二进制。
最后附上依据上面的算法用C写的计算十进制数补码的程序(8 bit),点击 这里下载
Leave a comment