你真的了解补码吗

1、在计算机内,有符号数有3种表示法:原码、反码和补码

有符号数在计算机中存储,用数的最高位存放符号, 正数为0, 负数为1
例如:有符号数 1000 0011,其最高位1代表负,其真正数值是 -3,而不是形式值131(无符号数1000 0011转换成十进制等于131)
原码:

原码就是符号位加上真值的绝对值,即用第一个二进制位表示符号(正数该位为0,负数该位为1),其余位表示值。

反码:
  • 正数的反码与其原码相同;
  • 负数的反码是对其原码逐位取反,但符号位除外。
补码:
  • 正数的补码就是其本身;
  • 负数的补码是在其反码的基础上+1

原码反码补码的定义,举个例子如下:

[+1] = [0000 0001]原 = [0000 0001]反 = [0000 0001]补
[-1] = [1000 0001]原 = [1111 1110]反 = [1111 1111]补 

2、计算机内,为何要使用补码

对于计算机而言,加减乘数是最基础的运算, 要设计的尽量简单。我们知道,根据运算法则,减去一个正数等于加上一个负数,即:1 - 1 = 1 + (-1) = 0,所以计算机内部可以只有加法而没有减法,这样计算机的设计就简单了,于是人们开始探索将符号位也参与运算,将减法用加法替代。

我们分别用 原码 反码 补码 来计算下1 - 1的结果:

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

我们可以看到,使用原码和反码都不能正确的进行1 - 1的减法运算。

补码的优点:

  • 可将减法变为加法,省去减法器;
  • 无符号数及带符号数的加法运算可以用同一电路完成;
  • 使用补码,修复了原码中0的符号(有 [+0] [-0] 之分)以及存在两个编码(0000 0000 和 1000 0000)的问题,而且还能够多表示一个最低数。

在8位二进制中:

  • 使用原码/反码表示的范围为[-127,+127],包含 [+0] [-0] 一共256个;
  • 使用补码表示的范围为[-128,+127],0没有符号。

3、补码的本质

补码为什么能正确实现加法运算呢?我们先从补码的由来开始说起

补码的由来

负数,其实就是0减去这个数的绝对值,比如 -8 = 0 - 8。8的二进制是 0000 0100,-8就可以用下面的式子求出:

  0 0 0 0 0 0 0 0 
- 0 0 0 0 1 0 0 0
-----------

因为[0000 0000](被减数)小于[0000 1000](减数),所以不够减。请回忆一下小学算术,如果被减数的某一位小于减数,我们怎么办?很简单,问上一位借1就可以了。

  1 0 0 0 0 0 0 0 0
 -  0 0 0 0 1 0 0 0
-----------
    1 1 1 1 1 0 0 0

进一步观察,可以发现1 0000 0000 = 1111 1111 + 1,所以上面的式子可以拆成两个:

  1 1 1 1 1 1 1 1
- 0 0 0 0 1 0 0 0
-----------各位取反
  1 1 1 1 0 1 1 1
+ 0 0 0 0 0 0 0 1
-----------加1
  1 1 1 1 1 0 0 0

以上就是-8的补码的转换过程。

因此负数的补码也可以表示为:1111 1111 + 1 - 负数的绝对值

用补码的方式将减法转换为加法的正确性验证

已知:X、Y都为正整数,Z = X - Y = X + (-Y)
证明:Z = X的补码 + (-Y的补码)

X的补码 = X; // 正数的补码为其自身
-Y的补码 = (1111 1111 - Y) + 1; // 负数的补码为除符号位的其它位取反加1
Z = X的补码 + (-Y的补码) 
  = X +  (1111 1111 - Y) + 1 
  = X - Y + 1 0000 0000
  = X - Y + 0000 0000
  = X - Y
// 1 0000 0000就相当于0000 0000(舍去了最高位) 

这就证明了,在正常的加法规则下,可以利用2的补码得到正数与负数相加的正确结果。换言之,计算机只要部署加法电路和补码电路,就可以完成所有整数的加法。

4、补码表示的溢出问题

同号数相加如果结果的符号位和两加数不同,既是溢出。

例如8bit的byte类型的表示范围为[-128,+127],那么+128、+129、-129、-130等超出范围的数该怎么表示呢?

 // 超上限 溢出
+128 = 127 + 1 = [0111 1111]补 + [0000 0001]补 = [1000 0000]补 = -128
+129 = 127 + 2 = [0111 1111]补 + [0000 0010]补 = [1000 0001]补 = [1111 1111]原 = -127
// 超下限  溢出
-129 = -128 + (-1) = [1000 0000]补 + [1111 1111]补 = [0111 1111]补 = 127
-130 = -128 + (-2) = [1000 0000]补 + [1111 1110]补 = [0111 1110]补 = 126

通过上述计算我们可以看出,对于8bit的数据(一共2^8 = 256个):

  • 超上限的数 x = x - 256;
  • 超下限的数 x = x + 256;
  • 下限的相反数与下限相等;
  • 上限的相反数是上限直接取负值。

证明举例如下:

byte a = -128, b = (byte) 128, c = (byte) 129, d = (byte) 130;
byte e = (byte) -129, f = (byte) -130;
System.out.println(a == ((byte)-a));    // true
System.out.println(b);  // -128
System.out.println(c);  // -127
System.out.println(d);  // -126
System.out.println(e);  // 127
System.out.println(f);  // 126

5、符号扩展与多重转型

下面的代码的输出是什么?能看懂结果么?

byte b = -1;
System.out.println((int)(char)b); // 65535
我们先来聊聊符号扩展(Sign Extension)

符号扩展用于在数值类型转换时扩展二进制位的长度,以保证转换后的数值和原数值的符号(正或负)和大小相同,一般用于较窄的类型(如byte)向较宽的类型(如int)转换。扩展二进制位长度指的是,在原数值的二进制位左边补齐若干个符号位(0表示正,1表示负)

Java中整型字面量种类
  • 十进制方式,直接书写十进制数字
  • 八进制方式,格式以0打头,例如012表示十进制10
  • 十六进制方式,格式为0x打头,例如0xff表示十进制255

需要注意的是,在Java中012和0xff返回的都是int型数据,即长度是32位。

Java类型转换规则(出自《Java解惑》一书)

1、如果最初的数值类型是有符号的,那么就执行符号扩展;
2、char是无符号类型,不管它要被转换成什么类型,都执行零扩展;
3、如果目标类型的长度小于源类型的长度,则直接截取目标类型的长度。例如将int型转换成byte型,直接截取int型的右边8位。

解析“多重转型”问题
(int)(char)(byte) -1
  • int(32位) -> byte(8位)
    -1是int型的字面量,编码结果为0xffff ffff,即32位全部置1。转换成byte类型时,直接截取最后8位,所以转换后的结果为0xff,对应的十进制值是-1。
  • byte(8位) -> char(16位)
    由于byte是有符号类型,所以在转换成char型(16位)时需要进行符号扩展,即在0xff左边连续补上8个1(1是0xff的符号位),结果是0xffff,对应的十进制数是65535(char是无符号类型)。
  • char(16位) -> int(32位)
    由于char是无符号类型,转换成int型时进行零扩展,即在0xffff左边连续补上16个0,结果是0x0000 ffff,对应的十进制数是65535

6、几个转型的例子

先看下面代码的输出:

byte b=-1;
System.out.println((int)(char)(b & 0xff)); // 255

1、byte型数值b -> char型:
char c = (char)(b & 0xff); // 不希望有符号扩展
char c = (char)b; // 希望有符号扩展

  • 0xff是int型字面量(0x0000 00ff),(b & 0xff)的结果是32位的int类型,前24被强制置0,后8位保持不变,然后转换成char型时,直接截取后16位。这样不管b是正数还是负数,转换成char时,都相当于是在左边补上8个0,即进行零扩展而不是符号扩展。
  • byte类型(8位)的b扩展成char型(16位)时需要进行符号扩展。

2、char型数值c -> int型:
int i = c & 0xffff; // 不希望有符号扩展
int i = (short)c; // 希望有符号扩展

  • 0xffff是int型字面量(0x0000 ffff),所以在进行&操作之前,编译器会自动将c(16位)转型成int型(32位),即在c的二进制编码前添加16个0,然后再和0xffff进行&操作,所表达的意图是强制将前16位置0,后16位保持不变。
  • 首先将c转换成short类型,它和char是 等宽度的,并且是有符号类型,再将short类型转换成int类型时,会自动进行符号扩展,即如果short为负数,则在左边补上16个1,否则补上16个0。

最后,介绍一种简单的 2的补码对应十进制数 的记忆方式:

0000 0000 = 0
那么在 0 的基础上 -1 就是(想象成借位减法):
1111 1111 = -1
以此类推:
1111 1110 = -2
1111 1101 = -3
...

参考资料:
你真的了解Java中的负数?
关于2的补码

推荐阅读更多精彩内容