007、原码, 反码, 补码

Par @Martin dans le
Tags :

所有整数在计算机中都是以补码来存储的

1. 符号位

首先搞清楚数值在内存中的存储方式, 对于有符号的整数, 在内存中, 用最高位来存放符号, 正数为0, 负数为1.

我们以单字节(byte)为例, 它在内存中占 8 位, 那个 0000 0001 表示 +1, 而 1000 0001 则表示 -1, 这就是原码的表示方式.

2. 原码, 反码, 补码

2.1 原码

原码, 刚已经表示过了, 即用最高位来表示符号, 其他位不动, 所以 byte 的取值范围是 -2^7(-128)~2^7-1(127), 为什么是 -128 到 127 呢?
因为在计算机中, 0 也属于正数(0000 0000), 所以 -128~-1 为负数区间,共 128 个数, 而 0~127 为正数区间, 也共 128 个数.

2.2 反码

反码的表示方法是:
正数的反码是其本身
负数的反码是在其原码的基础上, 符号位不变,其余各个位取反.

2.3 补码

补码的表示方法是:
正数的补码就是其本身
负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1(即在反码的基础上+1).

如果已知补码, 要求原码, 其推算过程和原码->补码一样, 把补码的符号位不变, 其余各位取反, 最后+1, 得到的即是原码(有名话叫: 补码的补码就是原码). 另外, 把补码减一再取反也能得到原码, 据说, 二进制中, “减一取反” 和 “取反加一” 是一样的, 我还没搞懂为什么…

2.4 为什么要用补码

首先, 对于计算机, 加减乘数已经是最基础的运算, 要设计的尽量简单. 计算机辨别”符号位”显然会让计算机的基础电路设计变得十分复杂! 于是人们想出了将符号位也参与运算的方法.
我们知道, 根据运算法则减去一个正数等于加上一个负数, 即: 1-1 = 1 + (-1) = 0 , 所以机器可以只有加法而没有减法, 这样计算机运算的设计就更简单了.
于是人们开始探索 将符号位参与运算, 并且只保留加法的方法. 首先来看原码:

计算十进制的表达式: 1-1=0

1 - 1 = 1 + (-1) = [00000001]原 + [10000001]原 = [10000010]原 = -2

如果用原码表示, 让符号位也参与计算, 显然对于减法来说, 结果是不正确的.
这也就是为何计算机内部不使用原码表示一个数.
为了解决原码做减法的问题, 出现了反码:

计算十进制的表达式: 1-1=0

1 - 1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原= [0000 0001]反 + [1111 1110]反 = [1111 1111]反 = [1000 0000]原 = -0

发现用反码计算减法, 结果的真值部分是正确的.
而唯一的问题其实就出现在 “0” 这个特殊的数值上, 虽然数学上 +0 和 -0 是一样的, 但是 0 带符号是没有任何意义的, 而且会有 [0000 0000]原 和 [1000 0000]原 两个编码表示0.
于是补码的出现, 解决了 0 的符号以及两个编码的问题:

1-1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原 = [0000 0001]补 + [1111 1111]补 = [0000 0000]补= [0000 0000]原 = 0

这样 0 用 [0000 0000] 表示, 而以前出现问题的 -0 则可以用来表示-128.

2.5 -128 怎么推算过来

我们已经知道, 负数的补码, 是先把正数的补码符号位变为 1, 然后 “取反加一” , 但是现在根本就没有 128 正数的补码(最高为 127), 该怎么得到 -128 的补码呢?

其实可以用 (-127) + (-1) 的方式来得到 -128 的补码.

(-1) + (-127) = [1000 0001]原 + [1111 1111]原 = [1111 1111]补 + [1000 0001]补 = [1000 0000]补

3. 溢出

int i = 128;
byte b = (byte)i;

请问 b 的值是多少?

int i = 128;
则转成补码应该是:
0000 0000 0000 0000 0000 0000 1000 0000

但如果把 128 强制赋给 byte 型, 即一个字节, 那么高位就被强制去除, 留下 10000000, 它是一个负数 -128, 这就是所谓的溢出.