欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【转载】你真的了解补码吗

程序员文章站 2024-03-23 15:54:16
...

原文:https://www.jianshu.com/p/3004e5999be4

解决了关于补码扩展位宽时符号位一直补最高位的疑问  看3.补码的本质

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
...



作者:DevWang
链接:https://www.jianshu.com/p/3004e5999be4
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。