基于问题 “-128 的二进制形式为什么表示为 1000 0000 ?” 展开讨论补码、反码以及原码之间的关系

(1)模以及模的运算

什么是“模”,数学里面的解释是:

“模”是一种整数的算术系统,当数字到达一定的值的时候,就会出现“环绕”( wrap around )。

其实可以把“模”理解为 一种计数范围,这种“计数范围”用于限制数的可计算范围。

比如时钟的“模”就 12,这个“模”将时钟的计量范围限制为是 0 ~ 12 。举个例子,如果现在是3点,如果你想将时间减少 5 小时,你可以逆时针拨 5 个小时到 10 点,你也可以顺时针拨 (12 - 5 = 7)个小时到 10 点,此处 (3 - 5)与 (3 + 7)得到的结果是一样的。

同样的,对于十进制的两位数,“模”就是100,那么 (70 - 30)与 (70 + 70)在不考虑百位的基础上,结果都是 40 。此处 30 与 70 为“模 = 100” 情况下的补数。同样的,当“模”为 100 的时候,40和60、66和34 ... 等互为补数。在“模”的范围内做减法,在忽略百位数的前提下,“A - B” 与 “A + B的补数”其实是一样的

前面都是“大数减小数”的例子,下面对“小数减大数”的情况进行分析。如 (30 - 70)= -40,而如果按上面的的理论,70 的补数是 30,那么应该有 (30 - 70)= -40 = (30 + 30)= 60,这很明显是错的。

为了解决这个问题,设计者就设计了一套规则:将原来 0~99 划分为2部分,其中 [0 , 49] 表示正数,而 [50 , 99] 用于表示各自补数的负值,如 60 的补数是 40,40 的负值是 -40,那么 此时就有 60 = -40

总而言之,在“模”的运算中, 负数的表达方式就是它绝对值的补数。


(2)基于“模”的二进制运算

对于八位二进制数,一共可以表示 28 = 256 个数,即 0 ~ 255,用二进制表示就是从 [0000 0000][1111 1111],此时“模”就是256。

按(1)中的逻辑,那么此时将 0 ~ 255 分为两部分,其中 0 ~ 127 表示正数,128 ~ 255 表示负数,即各自补数的负值,即 -128 ~ -1。 换为二进制表示,就是 [0000 0000][0111 1111] 表示正数,而 [1000 0000][1111 1111] 则表示各自补数的负值

此处,将 128 --- 255 称为各自补数负值的“补码”,如 177(二进制表示为 [1011 0001] )为其补数 77 的负值 -77 的补码

0000 0000 --- 0
0000 0001 --- 1
...
0111 1111 --- 127
1000 0000 --- -128(1000 0000 即 128 的补数是 128,负值为 -128 )
...
1111 1110 --- -2 (1111 1110 即 254 的补数是 2,负值为 -2 )
1111 1111 --- -1 (1111 1111 即 255 的补数是 1,负值为 -1 )

如上分析,由于 Java 中 byte 类型就是八位二进制表示,那么它值的范围就是 [-128 , 127]。

总而言之,转换的规则就是(下面的计算都是基于八位二进制,即“模”为256):

1)求某个负数的二进制正数表示形式(即补码):求该负值绝对值的补数。

如求 -77 的正数二进制表示,则求其绝对值的补数为 179,二进制表示为 [1011 0011] ,即 -77 在八位计算机中表示为 [1011 0011]

2)求某个正数(补码)表示的负值:求该正数的补数并取反

求 188 ,即 [1011 1100] 表示的负数,则求其补数并取反,得到 -68,即 [1011 1100] 在八位二进制计算机中表示 -68。

需要注意的是,所有的计算结果必须在 [-128 , 127] 的范围内,如果发生溢出,就会报错。因此编程的时候总是强调 byte + byte 还得是 byte(不考虑 Java 的自动类型提升),int + int 还是 int。


(3)-128 为什么表示为 1000 0000 ?

提问:我们知道计算机中的数都是用二进制补码的形式存储,为什么 -128 的补码形式是 [1000 0000],而按 补码 = 原码除符号位取反 + 1,[1000 0000] 按“除符号位减一取反”的规则是无法求得其原码与反码的。

:要搞清楚这个问题,我们得明确求 -128 等负数在计算机中的存储形式(即所说的“补码”)的目的,这是为了用正数来表示负数,这样就可以将计算机的减法转换为加法,此时减法计算 “A - B ” 与 加法计算 “A + (-B)的补码”相同。

直接按照上面的理论,求 -128 的补码,取 -128 的绝对值并求补数 256 - |-128|,得到128,即 [1000 0000] 就是 -128的补码。而由于上面的求补码的操作中, “模 - 绝对值 ” 也涉及减法,而计算机只有加法器。本来设计出补码的规则就是为了避开减法,但是理论上在计算负数补码的时候,还是需要减法逻辑。

因此,为了避开计算负数补码时的减法操作,前人们就定义了“首位为符号位”、“原码、反码”、“原码取反 + 1 得到补码”等规则,这样计算机就能在不使用减法操作的情况下通过负数的原码计算负数的补码,得到负数的补码后,又能将减法操作转换为加法操作。

如对于 120-114 = 120 + (-114) = [0111 1000] + (-114补码) ,需要计算 -114 (原码是 [1111 0010] )的补码,则除符号位外取反+1,得到 [1000 1110],表示的正数是142 ,然后 120-114 = [0111 1000] + [1000 1110] = [1 0000 0110],舍去溢出的第九位,结果为 [0000 0110],也就是 6 的补码。

在计算机中,如果想计算 -128 的补码,因为 -128 的原码表示应该为 [1 1000 0000] ,那么八位的数据结构(如 byte)便无法 -128 表示的原码与反码,按我的理解,此处 -128 的补码为 [1000 0000] 事实上应该是约定好的,我们无法在八位的数据结构中求得 -128 的反码与原码,但是这并不影响 [1000 0000] 在计算机表示 -128 参与计算。

答毕----------------------------------------------------------------

其实,上面说了那么多,其实只要明确各个环节直接的逻辑关系,就可以很容易将“原码、反码、补码”复杂的讨论简化:

(1)首先,因为计算机中没有减法器,为了将减法转换为加法,于是将“模区间”划分为2部分,用值较大的部分表示值较小部分对应的负数,如 byte 中将 [0 , 255] 划分为 [0 , 127]、[128 , 255],而 [128 , 255] 内的数就表示 [-128 , -1] 内对应数的补码。

(2)由(1)知,为了将减法转换为加法,需要计算求负数对应的补码,但是这个步骤同样需要用到减法。如计算 -128 的补码,需要 256 - |-128|,但是计算机无法进行减法操作。

(3)于是前人定义了“首位为符号位”、“原码、反码”、“原码取反+1得到补码”等规则,就是为了计算机可以在不使用减法的条件下,通过负数原码计算得到负数的补码。(即先有的补码,原码反码是计算机为了方便计算补码搞出来的

真正在使用的时候,只需要知道理论上如何求取一个负数对应的补码即可,至于原码、反码、补码之间的转换,那是计算机内部的逻辑,想太多容易搞混。


(4)原码、反码与补码的转换规则

计算机中的数字都是用二进制形式表示的,这些二进制数也叫这个数的机器数。机器数是带符号的,计算机中用一个二进制数的最高位存放符号,正数为0,负数为1。比如,十进制中的数 +3 ,若计算机字长为 8 位,转换成二进制就是 [0000 0011],如果是 -3 ,就是 [1000 0011]

因为第一位是符号位,所以机器数的形式值就不等于真正的数值。例如上面的有符号数 [1000 0011],其最高位 1 代表负,其真正数值是 -3 而不是形式值 131( [1000 0011] 转换成十进制等于131)。所以,为区别起见,将带符号位的机器数对应的真正数值称为机器数的真值。例:[0000 0001] 的真值 = +000 0001 = +1,[1000 0001] 的真值 = –000 0001 = –1。

对于一个数,计算机要使用一定的编码方式进行存储,原码、反码、补码是机器存储一个具体数字的编码方式。

1)原码

原码就是符号位加上真值的绝对值,即用第一位表示符号,其余位表示值。比如如果是 8 位二进制数:

正数:[+1] = [0000 0001]原 
负数:[-1] = [1000 0001]原

因为第一位是符号位,所以 8 位二进制数原码的取值范围就是:[1111 1111 , ... , 1000 0001 , 0000 0000 , 0000 0001 , ... , 0111 1111] ==> [-127 , ... , -1 , 0 , 1 , ... , 127]

2)反码

反码的定义:

  • 正数的反码是其本身
  • 负数的反码是在其原码的基础上, 符号位不变,其余各个位取反
正数:[+1] = [00000001]原 = [00000001]反
负数:[-1] = [10000001]原 = [11111110]反

3)补码

补码的定义:

  • 正数的补码就是其本身
  • 负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后 +1 .(即在反码的基础上 +1 )
正数:[+1] = [00000001]原 = [00000001]反 = [00000001]补
负数:[-1] = [10000001]原 = [11111110]反 = [11111111]补

事实上,计算机里都是用补码进行运算存储的,具体的逻辑和原因上面也有提及。原码和反码只是为了方便计算负数的补码,通过原码和反码,计算机就可以在不使用减法的情况下计算得到负数的补码。而补码也只是一种命名而已,事实上补码就是一个负数的正数表达形式,同样是为了使得计算机中的减法运算可以转换为加法运算。

参考文章:

  1. 在8位二进制中,-128 没有原码、反码形式,那么它的补码是怎么计算出来的?还是约定的?

个人水平有限,如有错误,欢迎各位朋友在评论区批评指正

posted @ 2022-02-24 16:19  ThinkingOverflow  阅读(542)  评论(0编辑  收藏  举报