原/反/补码与位运算的基础概念
零、参考资料
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 数值位是对原码数值位的 “按位取反” |
补码 | 无(或者认为是原码本身) | 在负数反码的基础上 + 1 |
- 原码:最简单的机器数,例如前文提到从 +1110 和 -1110 转换得到的 0000, 1110 和 1000, 1110 就是原码表示法,所以原码在进行数字运算时会存在前文提到的效率问题
- 反码:一般认为是原码和补码转换的中间过渡
- 补码:补码才是解决机器数的运算效率的关键,在计算机中所有 "整型类型" 的负数都会使用补码表示法
Javascript,可以使用 Number.toString(2) 来查看一个数的二进制值(字符串),但是如果是负数,并不是简单的补码表现形式,而是用数字绝对值的标准二进制代码(原码)前面加负号的形式输出:如 (-18).toString(2) === '-10010' ,这样做,似乎是 ECMAScript 想让开发者不必关心它们的用法
求负数补码的步骤:
- 得到这个负数的原码,如 -14 的原码是:1000 1110
- 保留符号位不变,将数值位取反,得到反码:1111 0001
- 将反码 + 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 |
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)
2019-05-22 Typescript - 变量类型