原/反/补码与位运算的基础概念

零、参考资料

Under-One、一些技术概念

机器数与真值

  • 真值:在日常的书写习惯中,往往用正、负号加绝对值表示数值,用这种形式表示的数值为真值。例如100D,-50D,-76O等
  • 机器数:在计算机内部中使用的数,并且最高位表示符号的二进制数,又分为有符号数和无符号数。例如正数 7 用 8 位二进制数来表示:0000 0111
  • 机器数与真值之间的转换会使用一些原/反/补码的概念

计算

  • 计算机中统一使用的是加法计算 - 计算机不会减法

一、问题的产生 - 机器数的运算效率问题

问题:机器数是带符号的,而两个数相加,符号位是否参加运算,以及是如何运算的?
  • 两个正数相加(均以原码表示):
    0000, 1110 + 0000, 0001 = 0000, 1111 // 14 + 1 = 15 正确
    ^            ^            ^
    符号位        符号位        符号位
  • 两个负数相加(均以原码表示):
    1000, 1110 + 1000, 0001 = 0000, 1111 // (-14) + (-1) = 15 错误
    ^            ^            ^
    符号位        符号位        符号位(最高位的 1 溢出)
可以看到,在对机器数进行 "按位加法" 运算时,只有两个正数的加法运算的结果是正确的,而包含负数的加法运算的结果却是错误的。那么如何解决这个问题呢,数学上,我们可以:
  • 两个负数相加:
  1. 用较大的绝对值 + 较小的绝对值(加法运算)
  2. 最终结果的符号为负
  • 正负数相加:
  1. 判断两个数的绝对值大小(数值部分)
  2. 用较大的绝对值 - 较小的绝对值(减法运算)
  3. 最终结果的符号取绝对值较大数的符号
但是,在电路设计时需要这么复杂吗。有没有不需要设置减法器,而且步骤简单的方案?

二、原码、反码、补码

为了解决有符号机器数运算效率问题,计算机科学家们提出多种机器数的表示法:
 
机器数 正数 负数
原码 符号位表示符号
数值位表示真值的绝对值
符号位表示数字的符号
数值位表示真值的绝对值
反码 无(或者认为是原码本身) 符号位为 1
数值位是对原码数值位的 “按位取反”
补码 无(或者认为是原码本身)  在负数反码的基础上 + 1
 
  • 原码:最简单的机器数,例如前文提到从 +1110 和 -1110 转换得到的 0000, 1110 和 1000, 1110 就是原码表示法,所以原码在进行数字运算时会存在前文提到的效率问题
  • 反码:一般认为是原码和补码转换的中间过渡
  • 补码:补码才是解决机器数的运算效率的关键,在计算机中所有 "整型类型" 的负数都会使用补码表示法
Javascript,可以使用 Number.toString(2) 来查看一个数的二进制值(字符串),但是如果是负数,并不是简单的补码表现形式,而是用数字绝对值的标准二进制代码(原码)前面加负号的形式输出:如  (-18).toString(2) === '-10010' ,这样做,似乎是 ECMAScript 想让开发者不必关心它们的用法
求负数补码的步骤:
  1. 得到这个负数的原码,如 -14 的原码是:1000 1110
  2. 保留符号位不变,将数值位取反,得到反码:1111 0001
  3. 将反码 + 1,得到补码:1111 0010
所以,拿到一个二进制数,其实首先应该问的是这个二进制是用何种形式表示的,是原码码值还是补码码值,然后在进行翻译。但是在实际中,负数一般都是用的补码码值,而正数的原码和补码又是一致的,所以我们在看到一个二进制数的时候,默认其是补码的表现形式
所以在上面的两个负数的计算中,我们用补码进行计算的过程是:
1111, 0010 + 1111, 1111 = 1111, 0001 // (-14) + (-1) = (-15) 正确
^            ^            ^
符号位        符号位        符号位(最高位的 1 溢出)
由此,使用补码表示法后,有符号机器数加法运算就只是纯粹的加法运算,不会因为符号的正负性而采用不同的计算方法,也不需要减法运算,简化了电路设计
同时,除了消除减法运算外,补码表示法还实现了 "0" 的机器数的唯一性:
在原码表示法中,"+0" 和 "-0" 都是合法的,而在补码表示法中 "0" 只有唯一的机器数表示,即 "0000, 0000" 。换言之补码能够比原码多表示一个最小的负数 1000, 0000

三、补码背后的规律

具体与数学中的补数相关,这里简单举个例子来描述下规律:

时钟的例子

比如说,现在时钟的时针刻度指向 6 点,我们想让它指向 3 点,应该怎么做?
  • 方法 1:逆时针地拨动 3 个点数,让时针指向 3 点,这相当于做减法运算 -3
  • 方法 2:顺时针地拨动 9 个点数,让时针指向 3 点,这相当于做加法运算 +9
可以看到,对于时钟来说 -3 和 +9 竟然是等价的。
这是因为时钟只能 12 个小时,当时间点数超过 12 时就会自动丢失,所以 15 点和 3 点在时钟看来是都是 3 点。如果我们要在时钟上进行 6 - 3 减法运算,我们可以将 -3 等价替换为它的正补数 +9 后参与计算,从而将减法运算替换为 6 + 9 加法运算,结果都是 3

十进制的一个例子

如果计算 354365 - 95937,用补数的规律该怎么做?
354365 - 95937 // = 258428

= 354365 - (1000000 - 904063)

= 354365 - 1000000 + 904063 // 减整加补

= 258428

 

这里存在的问题是:为何是 1000000 ?这个 1000000 就相当于是时钟中的 12,是当前运算中的最大位数上限(周期之类的概念),这个数据似乎还和进制相关(这个有空再研究),所以实际上,运算的过程是:
354365 + 904063 = 1258428 = 258428
                  ^
                  最高位 1 超出位数限制,直接丢弃

 

总结

补码的关键在于:找到一个与负数等价的正补数,使用该正补数代替负数,从而将减法运算替换为两个正数加法运算。
补码的出现与运算器的电路设计有关,从设计者的角度看,希望尽可能简化电路设计和计算复杂度。而使用正补数代替负数就可以消除减法器,实现简化电路的目的

四、位运算

位运算是基于二进制值的运算(补码表现形式)

Operator Usage Description
按位与 a & b 在 a,b 的位表示中,每一个对应的位都为 1 则返回 1,否则返回 0
按位或 a | b 在 a,b 的位表示中,每一个对应的位,只要有一个为 1 则返回 1,否则返回 0
按位异或 a ^ b 在 a,b 的位表示中,每一个对应的位,两个不相同则返回 1,相同则返回 0
按位非 ~ a 反转被操作数的位
左移 a << b 将 a 的二进制串向左移动 b 位,右边移入 0
算术右移(有符号右移,其实就是保持符号位不变,数值位右移) a >> b 把 a 的二进制表示向右移动 b 位,丢弃被移出的所有位
无符号右移(左边空出位用 0 填充) a >>> b 把 a 的二进制表示向右移动 b 位,丢弃被移出的所有位,并把左边空出的位都填充为 0

五、位运算在 Javascript 中的应用

posted @ 2024-05-22 16:31  shiweiqianju  阅读(60)  评论(0编辑  收藏  举报