美文网首页
你真的了解补码吗

你真的了解补码吗

作者: DevWang | 来源:发表于2017-08-20 16:26 被阅读0次

    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的补码

    相关文章

      网友评论

          本文标题:你真的了解补码吗

          本文链接:https://www.haomeiwen.com/subject/mvpurxtx.html