大学的时候就学了补码,但很长一段时间都没有理解。突然某一天看到 Java 的 byte 的类型表示的范围是,按照最高位是符号位,一个 byte 能表示的最大正数是 128(),然后负数是存的补码,最小能表示到 -128,那为什么是 -128 呢?这一下勾起了我的兴趣,我要去彻底弄明白这个东西。搜了一波网上的文章,看了之后反正是晕乎乎的,下面将以我的思路来表述一下我对补码的看法。
同余
钟表的例子
图1:
将时针从 10 点调到 5 点可以将时针逆时针方向拨 5 格,也可以顺时针方向拨 7 格,于是可以看成 和 是一样的效果:
-
时针逆时针方向拨 5 格,相当于做减法:
-
时针顺时针方向拨 7 格,相当于做加法:
这里的前提是有 12 这个数作为上限的,当和超过 12,则将 12 舍去。于是类推就有减 4 可表示成加 8,减 3 可表示成加 9……
很容易看出:
5 + 7 = 12
4 + 8 = 12
3 + 9 = 12
...
这些数对的和都一样,称这些数互补。
在数学中,用「同余」概念描述上述关系,两个整数 a、b,若它们除以整数 m 所得的余数相等,则称 a 与 b 对于模 m 同余,记作:
7 % 12 = -5 % 12 = 7,当 M = 12 时,-5 和 +7,-4 和 +8,-3 和 +9就是同余的。
综上:
基于这种模算术 Modular arithmetic,减一个数 x,可以转换成加这个数的 m - x(m 是模)。
二进制补码
以 4 个 bit 位来描述补码吧,4 bit 能表示 16 ()个数,16 是模。模也就是能表示的数的个数,这些数都可以在一个环上进行表示,以易于我们的理解。现在假设表示的这 16 个数都是无符号的整数,现在把这 16 个数像钟表一样在一个环上表示:
图2:
如图,从钟表的例子继续我们的减法运算,假设现在有一个数 x,有如下的计算式的转换:
x - 8 => x + 8
x - 7 => x + 9
x - 6 => x + 10
x - 5 => x + 11
...
计算补码:
/ X 0 <= X <= +7 正数和 0 的补码,就是该数字本身
[X]补 = |
\ 2^4 -|X| -8 <= X < 0 负数的补码,就是用 10000,减去该数字的绝对值
-8 的补码是 8(1000),-7 的补码是 9(1001),-6 的补码是(1010)……
既然减法能转换成加一个补码,那就把这个补码存到计算机中,这样计算机中就没有减运算了。
图3:
上图中的二进制就是计算机中所存储的数据,首位为 0 的表示正数,最多能表示,不能表示 +8,但可以用补码()表示一个负数 -8(1000)。4 bit 里最小的补码是 1000,最大的补码是 1111。
图2 和 图3 对应的二进制码是没有变化的。图3 变化的是首位为 1 的二进制码都是负数的补码。
看图3,在模是 ,4 bit 所能表示的数都在环上标出,x 和 y 是环上的点,
所有的 x -y 都转换成 x + (-y),-y 用补码表示,直接将 x 的二进制和 -y 的补码二进制相加即可得到 x-y 的结果。
通过负数的补码将减法转换为加法,出现的进位就是模,舍去即可。
4 - 2 = 2
0100
+ 1110
----------
10010
这里出现了进位,舍去进位。
结果 10010 超过模了,其实舍去就相当于 10010 - 10000(类似mod M)= 0010。
在圆环里可以很清晰的看出超过模之后就开始新一轮的计数。
总结
模是 m,在模的基础上,所有要表示的数都可以连续的分布在一个环上,有一个指针(这里假想出来的,方便下面的描述),
x 和 a 都是环上的点, a > 0,x - a 的值就是将指针逆时针拨动 a 格之后指向的值,和顺时针 从 x 处拨动 m - a 格所指向的值是一样的。
-a 和 m - a 对于模 m 同余,m-a 是 -a 的补码。于是计算机中用 m-a 表示 -a,
x - a = x + m - a (模是 m )。
这是我所理解的补码。
最后
4 位二进制数的模是 ,
8 位二进制数的模是 ,
16 位二进制数的模是 ,
32 位二进制数的模是 ,
4 位二进制补码最多能表示 (16 个数),数的范围是 ,
8 位二进制补码最多能表示 (256 个数),数的范围是 ,
16 位二进制补码最多能表示 (65536 个数),数的范围是 ,
32 位二进制补码最多能表示 个数,数的范围是 。
参考:
Why Two’s Complement works
从计算机为什么用补码存储数据,衍生到存储单元数据溢出
Class #7 - Signed Binary Numbers, Subtraction and Overflow
原码、反码和补码