计算机中运算为何需要反码和补码

Demon.Lee 2020年10月18日 2,243次浏览

问题

正如标题所抛出的问题,为何计算机中的运算需要用补码?如果换个问法就是:补码是如何出现的,它又是如何解决运算问题的。

概念

因为计算机的一切都是通过0和1来表示,也就是二进制。而数值又分为有符号数和无符号数,无符号数理解起来,则要相对简单一些,没有符号位,即所有的二进制位都参与值计算,也就是说无符号数表示的都是正数,比如c语言中的unsigned int。

有符号数:数值的最高位表示正负,1表示负,0表示正数,以8位的二进制数为例,10001010表示负数,而00000101则表示正数;
无符号数:最高位是值的一部分,比如10001010换算成10进制就是138。

而有符号数的表示有3种方式:原码、反码和补码。
这里我们先只给出原码概念:

原码:即符号位用 0 表示正数,而用 1 表示负数,其他位表示数值本身。比如:10001010表示-10,00000101表示+5。

分析

在分析之前,首先要明确两点(可以理解为定理):

  1. CPU硬件设计时,只有加法运算,没有减法运算,那如何做减法?减一个数看成加一个负数,即:a-b=a+(-b),其中b为正数。
  2. 数学中,数可以无限大,也可以无限小,但计算机容量有限,表示不了。故每一种数值类型的都是在一个区间之内的,即:[c, d],其中c表示下限,d表示上限。
    比如,java中byte是8位的有符号数据类型,表示值的范围:[-128, +127],写了个简单的运算例子,让我们先有一个感性的认识:
@Slf4j
public class TestPrimitiveDataType {

    @Test
    public void testByte() {
        byteSum((byte) 5, (byte) -3);
        byteSum((byte) 1, (byte) 127);
        byteSum((byte) -1, (byte) -128);
        byteSum((byte) 3, (byte) 256);
    }

    private void byteSum(byte byte1, byte byte2) {
        byte byte3 = (byte) (byte1 + byte2);
        log.info("byte sum: ({}) + ({}) = {}", byte1, byte2, byte3);
    }
}

// output:【运行环境:jdk-14.0.1】
00:03:42.689 [main] INFO org.learning.practice.tmp.TestPrimitiveDataType - byte sum: (5) + (-3) = 2
00:03:42.696 [main] INFO org.learning.practice.tmp.TestPrimitiveDataType - byte sum: (1) + (127) = -128
00:03:42.696 [main] INFO org.learning.practice.tmp.TestPrimitiveDataType - byte sum: (-1) + (-128) = 127
00:03:42.696 [main] INFO org.learning.practice.tmp.TestPrimitiveDataType - byte sum: (3) + (0) = 3

从运算结果可以看出:上限127再加1就变成了下限-128,而下限-128再加上-1则又变成了上限127。咦?我们好像发现了什么。

溢出

既然,数值类型表示的数值有一定的范围,那么计算结果超出了这个范围,会发生什么呢?答案是:溢出

这也很好理解,要表示的值,计算机做不到,只能出错了。那么问题来了,溢出之后,如何表示对应的值呢?我的总结是:

起始亦是终,首尾互相连。

计算机规定:n位有符号的二进制数,可以表示的取值范围是[-2^(n-1), 2^(n-1) -1]。如果超过上限叫上溢出(overflow),则从下限-2^(n-1) 开始;超过下限叫下溢出(underflow),则从上限2^(n-1)-1开始。


以4位的二进制有符号数为例,对应的取值范围是:[-8, 7],我们将其想象成一个圆:【手工绘制,不太好看,(^o^)】

上图,其实就跟我们的时针是一个道理了,超过了12点,就又变成了1点。

Q: 为什么n位有符号数是取值范围是[-2^(n-1), 2^(n-1)-1]?
A:我们以8位有符号数来说明的话,问题就变成了:为什么8位有符号数是取值范围是[-128,+127],而不是[-128,+128]或[-127,+127]?
1)如果是8位无符号数,则表示的数是:0000 0000 ~ 1111 1111,也就是[0, 255],一共256个数,即2^8;
2)而8位有符号数,其第一位是符号位,那么表示正数就是:0000 0000 ~ 0111 1111,即[+0, 127],一共128个数,正好是上面256个数的一半;
同样,负数应该也是128个,那么就是[-1,-128],但如果我们用原码来解释:1000 0000 ~ 1111 1111,则范围是[-0, -127],但这样的话,就出现了两个0了(+0, -0),
而1000 0000等于0111 1111 再加1,前辈们就把1000 0000表示为-128,而不是-0。
3)另外,【-128,+128】或【-127,+127】分别是257、255个数,不是256个数,也是不合理的。

原码–>反码–>补码

但是原码可以解决我们的负数运算问题吗?我们来试试:

  // 计算: 5 + (-3)
  
    0000 0101  // 5的原码
  + 1000 0011  // -3的原码
  ------------
    1000 1000  // -8
  

可以看到:5 + (-3) = -8,这是错误的。所以用原码来表示负数,并进行运算是不行的。

要解决这个问题,还是要重新回到前面的溢出中找答案。从上面4位二进制有符号数的图上可以看出来,数字5往前走16格,就又回到了5,这就相当于取模的概念了。而上限7减去下限-8再加1就是这个取模的除数了:7-(-8)+1=16。

如果[c, d]表示一个数据类型的下限和上限,那么 c-d+1 就是这个数据类型用来取模的除数。
n位二进制有符号数用来取模的除数就等于:2^(n-1)-1 - (-2^(n-1)) + 1 = 2^n 。但2^n 超出了n位二进制数可以表达的上限,所以我们写成:(2^n)-1+1。
在微机原理中,(2^n)-1+1 直接写成了(2^n),并命名为n位二进制数的模。

那么,5+(-3)=5+(-3)+(2^4) -1+1=5+((2^4)-1-3+1)

    // 计算 (2^4)-1-3
    111  // (2^4)-1 如果用二进制原码表示就是:1111,不考虑符合位就是111
  - 011  // 3
  -------
    100  // 如果算上符号位,结果就是1100

从结果可以知道,(2^4)-1-3 其实就是对3的原码0011进行了取反,得到1100。
如果我们将(2^4) -1-3命名为-3的 反码,那么继续推导:5+(-3)=5的原码+((2^4)-1-3+1)=5的原码+(-3的反码+1);
如果我们将“-3的反码+1”命名为-3的 补码,那么就得到:5+(-3)=5的原码+(-3的反码+1)=5的原码+(-3的补码)。
我们用i-j来替换5-3,其中i和j都是正数,那么就可以得到:

i-j = i的原码 + ((2^n)-1-j+1) = i的原码 + (-j的反码 + 1) = i的原码 + (-j的补码);
进一步,我们将正数的反码和反码都定义跟原码相同,那么:i-j = i的补码 + (-j的补码),从而也就能推导出:i+j= i的补码 + j的补码。

最后,我们以4位的二进制数为例再来理解一遍补码:

0000  0
0001  1
0010  2
0011  3
0100  4
0101  5
0110  6
0111  7   // 上限
1000  -8  // 下限
1001  -7
1010  -6
1011  -5
1100  -4 
1101  -3
1110  -2
1111  -1
0000  0  // 回到了0
...

下溢出,-8 + (-1) --> 1000 + 1111 = 0111,也就是上限7。

于是前辈们便总结出来了补码的取值:

image

结论

  • 计算机无法表示数学中的无限大和无限小的数,所以每类数据类型都有一个取值的数据范围,即:[-2^(n-1), 2^(n-1)-1]。
  • CPU中没有减法运算,我们通过加上一个负数来实现减法,并且利用溢出机制,发现可以用补码来实现加法。

未完待续

这篇文章断断续续写了一个星期,但还是没有完全搞懂,虽然结论我记住了。我想着,还是隔一段时间再回来想一想,说不定能闹明白。

  1. 上面的推导是用一个正数加上一个负数推导出来的,那两个正数的补码相加,如果溢出了,为何正好就等于负数的补码?
  2. 上溢出之后从下限开始,下溢出之后从上限开始,有的文章解释说,这是根据补码推导出来的,但我们这里是根据溢出推导出的补码,有点矛盾。

参考资料

  1. 黄申. 程序员的数学基础课:我们为什么需要反码和补码. 极客时间.
  2. 李继灿. 新编16/32位微型计算机原理及应用(第4版). 清华大学出版社,2008,34-40.
  3. 阮一峰. 关于2的补码. 2009年8月5日
  4. 计算机补码运算背后的数学原理是什么?知乎.