原码、反码、补码和位移运算

原码、反码、补码和位移运算

June 8, 2021

原码、反码和补码

为运算方便,机器数有 3 种表示法,即原码、反码和补码。

原码

原码是一种计算机中对数字的二进制定点表示法。原码表示法在数值前面增加了一位符号位(即最高位为符号位):正数该位为 0,负数该位为 1(0 有两种表示:+0 和 -0),其余位表示数值的大小。

反码

一个数字用原码表示是容易理解的,但是需要单独一个位来表示符号位,并且在进行加法时,计算机需要先识别某个二进制原码是正数还是负数,识别出来之后再进行相应的运算。这样效率不高,能不能让计算机在进行运算时不用去管符号位,也就是让符号位参与运算。要实现这个功能,就要用到反码。

反码是一种在计算机中数的机器码表示。对于单个数值(二进制的 0 和 1)而言,对其进行取反操作就是将 0 变为 1,1 变为 0。正数的反码和原码一样,负数的反码就是在原码的基础上符号位保持不变,其他位取反。

十进制 原码 反码
6 0000 0110 0000 0110
-3 1000 0011 1111 1100

下面我们来看一下,用反码直接运算会是什么情况,我们以 6 - 3 为例,6 - 3 等价于 6 + (-3)。

6 - 3 ==> 6 + (-3)
  0000 0110 // 6(反码)
+ 1111 1100 // -3(反码)
----------------------
  0000 0010 // (反码)
  0000 0010 // 2(原码)   

很明显通过反码进行 6 + (-3) 加法运算时,输出值比预期值差了一个 1。接着再来看下 1 + (-1) 的运算结果:

1 - 1 ==> 1 + (-1)
  0000 0001 // 1(反码)
+ 1111 1110 // -1(反码)
----------------------
  1111 1111 // (反码)
  1000 0000 // -0(原码)

由上可知 1 + (-1) 的运算结果为 -0,而我们预期的值是 +0。我们继续看个示例 0 + 0:

0 + 0 ==> 0 + 0
  0000 0000 // 0(反码)
+ 0000 0000 // 0(反码)
----------------------
  0000 0000 // (反码)
  0000 0000 // 0(原码)

这里可以知道 -0 对应的原码是 1000 0000,而 +0 对应的原码是 0000 0000。虽然 -0 和 +0 代表的数值是一样的,但是在用原码和反码表示时它们是不同的。通过以上的多个示例,我们发现使用反码进行加法运算并不能保证得出正确的结果。原因是用一个字节表示数字的取值范围时,这些数字中多了一个 -0。为了解决反码出现的问题,就出现了补码。

补码

补码是一种用二进制表示有符号数的方法。正数和 0 的补码就是该数字本身。负数的补码则是将其对应正数按位取反再加 1。

补码系统的最大优点是可以在加法或减法处理中,不需因为数字的正负而使用不同的计算方式。只要一种加法电路就可以处理各种有符号数加法,而且减法可以用一个数加上另一个数的补码来表示,因此只要有加法电路和补码电路即可以完成各种有符号数加法和减法,在电路设计上相当方便。

另外,补码系统的 0 就只有一个表示方式,这和反码系统不同(在反码系统中,0 有两种表示方式),因此在判断数字是否为 0 时,只要比较一次即可。下图是一些 8 位补码系统的整数,它可表示的范围包括 -128 到 127,总共 256 个整数。

既然说补码可以解决反码在运算中遇到的问题,继续以 6 + (-3) 为例来验证一下这个结论。

十进制	原码	反码	补码
6	0000 0110	0000 0110	0000 0110
-3	1000 0011	1111 1100	1111 1101
6 + (-3) 以补码形式的计算过程如下:

6 - 3 ==> 6 + (-3)
  0000 0110 // 6(补码)
+ 1111 1101 // -3(补码)
----------------------
  0000 0011 // 3(补码)

很明显这时得到了正确的结果,那么再来看一下以补码形式计算 1 - 1 的计算过程:

1 - 1 ==> 1 + (-1)
  0000 0001 // 1(补码)
+ 1111 1111 // -1(补码)
----------------------
  0000 0000 // 0(补码)

可以发现,补码完美解决了反码的问题

位移运算

位移运算需要使用按位移动操作符,它有两个操作数:第一个是要被移动的数字,而第二个是要移动的长度。移动的方向根据操作符的不同而不同。

按位移动会先将操作数转换为大端字节序顺序(big-endian order)的 32 位整数,并返回与左操作数相同类型的结果。右操作数应小于 32 位,否则只有最低 5 个字节会被使用。

左移(«)

该操作符会将第一个操作数向左移动指定的位数。向左被移出的位被丢弃,右侧用 0 补充。以 9 « 2 为例:

     9 (base 10): 00000000000000000000000000001001 (base 2)
                  --------------------------------
9 << 2 (base 10): 00000000000000000000000000100100 (base 2) = 36 (base 10)

在数字 x 上左移 y 位时,得出的结果是 x * 2^y,即 9 « 2 = 9 * 2^2。

有符号右移(»)

该操作符会将第一个操作数向右移动指定的位数。向右被移出的位被丢弃,拷贝最左侧的位以填充左侧。由于新的最左侧的位总是和以前相同,符号位没有被改变。所以被称作 “符号传播”。

例如, 9 » 2 得到 2:

     9 (base 10): 00000000000000000000000000001001 (base 2)
                  --------------------------------
9 >> 2 (base 10): 00000000000000000000000000000010 (base 2) = 2 (base 10)

相比之下, -9 » 2 得到 -3,因为符号被保留了。

     -9 (base 10): 11111111111111111111111111110111 (base 2)
                   --------------------------------
-9 >> 2 (base 10): 11111111111111111111111111111101 (base 2) = -3 (base 10)

无符号右移(»>)

该操作符会将第一个操作数向右移动指定的位数。向右被移出的位被丢弃,左侧用 0 填充。因为符号位变成了 0,所以结果总是非负的。

对于非负数,有符号右移和无符号右移总是返回相同的结果。例如 9 »> 2 和 9 » 2 一样返回 2:

      9 (base 10): 00000000000000000000000000001001 (base 2)
                   --------------------------------
9 >>> 2 (base 10): 00000000000000000000000000000010 (base 2) = 2 (base 10)

但是对于负数却不尽相同。 -9 »> 2 产生 1073741821 这和 -9 » 2 不同:

      -9 (base 10): 11111111111111111111111111110111 (base 2)
                    --------------------------------
-9 >>> 2 (base 10): 00111111111111111111111111111101 (base 2) = 1073741821 (base 10)
 -9 >> 2 (base 10): 11111111111111111111111111111101 (base 2) = -3 (base 10) 

参考:

最后更新于