Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

《关于补码》 #22

Open
wangsiyuan0215 opened this issue Feb 7, 2021 · 1 comment
Open

《关于补码》 #22

wangsiyuan0215 opened this issue Feb 7, 2021 · 1 comment

Comments

@wangsiyuan0215
Copy link
Owner

wangsiyuan0215 commented Feb 7, 2021

负数在计算机中如何表示呢?

在计算机内部采用2的补码(Two’s Complement)表示负数。

本文中以 8 位机作为计算基础。

什么是补码?

它是一种数值的转换方法,要分二步完成:

  • 第一步,每一个二进制位都取反值,0 变成 1,1 变成 0。比如:0000 1000 取反为 11110111
  • 第二步,将上一步的得到的值 +1。即:
11110111 + 1 = 11111000

所以,1111 10000000 1000 的补码,也就是说 -8 在计算机中用 1111 1000 表示;

为什么要用补码来表示负数呢?

对于计算机,加减乘除是最基本的运算,要尽量设计的很简单;

  • 运算法则减去一个数等于是加上这个数的负数;
  • 计算机的基础电路只有加法,使得设计更加简单;

由于对于二进制的编码有三种方式:原码、反码和补码;

  • 原码:进行正数 + 负数的类减法所得的结果并不正确;
  • 反码:进行计算时,结果的真值部分是正确的,但是会有一个特殊的值:0。由于 0 是具有符号位是没有任何意义的,并且会有 1000 00000000 0000 两个编码表示 0;
  • 补码:进行计算可以完美解决真值、符号位和0的问题,同时由于 (-1) - (-127) = -128,补码会以 1000 0000 表示 -128,即还能够表示一个最低数。
  • 原码和反码表示的范围为 [ -127, 127 ],而补码可以表示 [ -128, 127 ];

补码的本质:

负数可以换成是 0 - 正数 得到的,因此 对于 -8 可以看做是 0 - (+8) 得到的;
那么对于二进制的来说即如下形式:

           0 0 0 0 0 0 0 0
        -  0 0 0 0 1 0 0 0
    -------------------------- 
           1 1 1 1 1 0 0 0

因为 被减数 0000 0000 小于减数 0000 1000,因此需要向上一位借 1,因此实质上被减数是 1 0000 0000

          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 0

进一步观察可以发现,1 0000 0000 是由 1111 1111 + 0000 0001 所得,因此:

            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 0 0 0

补码的转换步骤就是这么来的;

@wangsiyuan0215
Copy link
Owner Author

wangsiyuan0215 commented Feb 7, 2021

《由 indexOf 引发的对于 ~ 的思考》

场景

当编写业务代码时,通常会用到 indexOf 来判断 target 是否在 source 中类似的问题。

那么最通常的做法是:

const index = source.indexOf(target);

if (index !== -1) {
  // 存在 target 的限定条件执行的逻辑
} else {
  // 不存在 target 的限定条件执行的逻辑
}

如何让类似的代码被写得很优雅?使用位运算的取反(~)操作!先来看下如下代码:

if(~source.indexOf(target)) {
  // 存在 target 的限定条件执行的逻辑
} else {
  // 不存在 target 的限定条件执行的逻辑
}

仅通过一个 “~” 符号就可以完成判断。那么这么写的依据是什么呢?

依据主要有两个:

  1. 位运算 ~
  2. 隐式转换;

隐式转换

具体隐式转换规则可参考我的这篇文章

位运算 ~

前提:我之前一直很困惑,JavaScript 的数字 Number 类型在计算机中是以多少位进行存储的呢。

JavaScript 采用“IEEE 754 标准定义的双精度64位格式”("double-precision 64-bit format IEEE 754 values")表示数字。

原来,JavaScript 在计算机中主要是以双精度 64 位浮点型格式表示 Number 类型,但是在进行位运算(数组索引等)操作的时候是以 32 位有符号位的二进制进行计算的。

这也是为什么 JS 关于小数计算的问题上总是会有丢失精度的问题。

位运算 ~ 取反操作是将二进制数字按位取反,但是因为二进制数字是有符号位的,因此在取反的过程中,符号位也被取反了。

举个🌰:

Q. 对数字 0 进行取反操作,最终得到的数字是?即 ~0 => ?

// ~0:
  // 先将数字 0 转换为 32 位的二进制(A):0000 0000 0000 0000 0000 0000 0000 0000
  // 然后对二进制 A 进行取反得到二进制(B):~(0000 0000 0000 0000 0000 0000 0000 0000) => 1111 1111 1111 1111 1111 1111 1111 1111
  // 由于有符号位的二进制的符号位位于从右往左的最后一位(31位),如果为 1 则表示负数,如果为 0 则表示正数
  // 二进制 B 的第 31 位是 1,因此当前二进制表示的负数,那么要求出最后的负数是多少,则需要进行逆向转换补码
  // 补码的具体步骤就是负数绝对值的二进制按位取反,然后加 1,得到的二进制数就是当前负数在计算机中的表示(不太清楚的小伙伴请参考上文)
  // 那么我们将二进制 B 减 1 然后再取反,就能得到当前负数的绝对值了
  // 1111 1111 1111 1111 1111 1111 1111 1111 - 1 => 1111 1111 1111 1111 1111 1111 1111 1110 => ~(1111 1111 1111 1111 1111 1111 1111 1110) => 0000 0000 0000 0000 0000 0000 0000 0001 => 1
  // 绝对值为 1 的负数很显然,就是 -1。

那么通过大量的测试,可以总结出来:一个数的取反操作最终对应的数字就是当前数的负数 - 1(同样对应负数)。

总结

回到业务场景,~source.indexOf(target)

  • 如果 indexOf 返回得是 -1,那么经过取反操作,会返回 0,而 0 被隐式转换后得到 Boolean 类型的值是 false
  • 如果 indexOf 返回得不等于 -1,那么经过取反操作后不会得到 0,而非 0 值被隐式转换后得到 Boolean 类型的值均是 true

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant