【问题标题】:Why does bitwise "not 1" equal -2?为什么按位“非 1”等于 -2?
【发布时间】:2017-10-15 04:46:47
【问题描述】:

假设我们有1,这个以 2 为底的数字是:

00000000000000000000000000000001

现在我想翻转所有位以获得以下结果:

11111111111111111111111111111110

据我所知,解决方案是使用~(按位非运算符)翻转所有位,但~1的结果是-2

console.log(~1); //-2
console.log((~1).toString(2)); //-10 (binary representation)

为什么我会得到这个奇怪的结果?

【问题讨论】:

  • 11111111111111111111111111111110-2
  • 11111111111111111111111111111111-1 > ~0 === -1
  • @AfshinMehrabani 没错,因为parseInt 第一个参数必须是字符串。 parseInt('11111111111111111111111111111110', 2) | 0
  • Bitwise operators: "所有按位运算符的操作数都转换为二进制补码格式的有符号 32 位整数。二进制补码格式意味着数字的负数对应物(例如 5 与 -5)是所有数字的位反转(按位非数字,也就是数字的反码)加一。”
  • 在纸上用手加两个,你会看到它变为零。加二等于零的数字应该称为减二。

标签: javascript numbers bitwise-operators


【解决方案1】:

1-2 之间有 2 个整数:0-1

1二进制是00000000000000000000000000000001
0二进制是00000000000000000000000000000000
-1二进制是11111111111111111111111111111111
-2二进制是11111111111111111111111111111110
("binary" 是 2 的补码,按位不是 ~ )

如您所见,~1 等于 -2 并不奇怪,因为 ~0 等于 -1

作为@Derekexplained,这些bitwise operators 将其操作数视为32 位序列。另一方面,parseInt 没有。这就是为什么你会得到一些不同的结果。


这是一个更完整的演示:

for (var i = 5; i >= -5; i--) {
  console.log('Decimal: ' + pad(i, 3, ' ') + '  |  Binary: ' + bin(i));
  if (i === 0)
    console.log('Decimal:  -0  |  Binary: ' + bin(-0)); // There is no `-0`
}

function pad(num, length, char) {
  var out = num.toString();
  while (out.length < length)
    out = char + out;
  return out
}

function bin(bin) {
  return pad((bin >>> 0).toString(2), 32, '0');
}
.as-console-wrapper { max-height: 100% !important; top: 0; }

【讨论】:

  • 酷。还有一个问题,如果11111111111111111111111111111110-2,那么为什么parseInt('11111111111111111111111111111110', 2)4294967294
  • @AfshinMehrabani 仅位运算符 treat operands as 32 bit integer。毫不奇怪,这就是为什么在 ~ 解析时得到 -2 而在解析时得到 4294967294 的原因。
  • @AfshinMehrabani 这就是有符号整数和无符号整数之间的区别。
  • 从数学上讲,在上面的每一行中,上面写着“以 2 为基数”的地方实际上应该说“在 2 的补码中”。以 2 为底的 -2 是 -10,只有在计算机中才有存储符号的方法。此外,还有一些无符号数据类型仍然以 2 为基数,但当然不是 2 的补码。
  • @ToddWilcox:从数学上讲,这不仅仅是二进制补码。要从任何数字中减一,请从右侧扫描,将零变为一,直到遇到一。将其更改为零并停止。从零中减去一将导致所有零变为一。二进制补码意味着数字的 MSB 应扩展到左侧的所有位。一个补码意味着它应该扩展到左侧 右侧的所有位(请注意,在二进制中,0.11111111...(无限)任意接近 1,因此 ....11111111.11111111.. ...(在两个方向上无穷无尽)任意接近 0。
【解决方案2】:
100 -4
101 -3
110 -2
111 -1
000  0
001  1
010  2
011  3

记住二进制补码符号如何工作的一种简单方法是想象它只是一个普通的二进制文件,除了它的最后一位对应于相同的取反值。在我设计的三位二进制补码中,第一位是1,第二位是2,第三位是-4(注意减号)。

如您所见,按位不在二进制补码中的是-(n + 1)。令人惊讶的是,将它应用于一个数字两次会得到相同的数字:

-(-(n + 1) + 1) = (n + 1) - 1 = n

按位说话很明显,但在算术效果上就不那么明显了。

还有一些观察可以让你更容易记住它的工作原理:

注意负值如何上升。完全相同的规则,只是交换了 0 和 1。如果您愿意,请按位未标记。

100 -4  011 - I bitwise NOTted this half
101 -3  010
110 -2  001
111 -1  000
----------- - Note the symmetry of the last column
000  0  000
001  1  001
010  2  010
011  3  011 - This one's left as-is

通过将二进制列表循环其中数字总数的一半,您可以获得从零开始的典型升序二进制数序列。

-  100 -4  \
-  101 -3  |
-  110 -2  |-\  - these are in effect in signed types
-  111 -1  / |
*************|
   000  0    |
   001  1    |
   010  2    |
   011  3    |
*************|
+  100  4  \ |
+  101  5  |-/  - these are in effect in unsigned types
+  110  6  |
+  111  7  /

【讨论】:

    【解决方案3】:

    在计算机科学中,一切都与解释有关。对于计算机而言,一切都是可以以多种方式解释的位序列。例如,0100001 可以是数字 33 或 !(这就是 ASCII 映射此位序列的方式)。

    对于计算机而言,一切都是位序列,无论您将其视为数字、数字、字母、文本、Word 文档、屏幕上的像素、显示的图像还是硬盘上的 JPG 文件。如果你知道如何解释那个位序列,它可能会变成对人类有意义的东西,但在 RAM 和 CPU 中只有位。

    因此,当您想在计算机中存储一个数字时,您必须对其进行编码。对于非负数,这非常简单,您只需要使用二进制表示即可。但是负数呢?

    您可以使用一种称为二的补码的编码。在这种编码中,您必须决定每个数字将有多少位(例如 8 位)。 most significant bit 保留为符号位。如果是0,则该数字应解释为非负数,否则为负数。其他 7 位为实际数字。

    00000000 表示零,就像无符号数一样。 00000001 是一,00000010 是二,依此类推。您可以在二进制补码的 8 位上存储的最大正数是 127 (01111111)。

    下一个二进制数 (10000000) 是 -128。这可能看起来很奇怪,但稍后我会解释为什么它是有意义的。 10000001 是 -127,10000010 是 -126,依此类推。 11111111 是-1。

    我们为什么要使用这种奇怪的编码?因为它有趣的特性。具体来说,在执行加法和减法时,CPU 不必知道它是一个存储为二进制补码的有符号数。它可以将这两个数字解释为无符号数,将它们相加,结果将是正确的。

    让我们试试这个:-5 + 5。-5 是 11111011500000101

      11111011
    + 00000101
    ----------
     000000000
    

    结果是 9 位长。最重要的位溢出,我们留下了00000000,它是 0。它似乎可以工作。

    另一个例子:23 + -7。 23 是00010111,-7 是11111001

      00010111
    + 11111001
    ----------
     100010000
    

    同样,MSB 丢失了,我们得到 00010000 == 16。它有效!

    这就是二进制补码的工作原理。计算机内部使用它来存储有符号整数。

    您可能已经注意到,在二进制补码中,当您对数字N 的位求反时,它会变成-N-1。例子:

    • 0 否定 == ~00000000 == 11111111 == -1
    • 1 否定 == ~00000001 == 11111110 == -2
    • 127 否定 == ~01111111 == 10000000 == -128
    • 128 否定 == ~10000000 == 01111111 == 127

    这正是您所观察到的:JS 假装它正在使用二进制补码。那么为什么parseInt('11111111111111111111111111111110', 2) 是4294967294?好吧,因为它只是假装。

    在内部 JS 总是使用浮点数表示。它的工作方式与二进制补码完全不同,它的按位取反几乎没有用,所以 JS 假装一个数字是二进制补码,然后取反它的位并将其转换回浮点表示。 parseInt 不会发生这种情况,因此您会得到 4294967294,即使二进制值看似相同。

    【讨论】:

      【解决方案4】:

      2 的补码 32 位有符号整数(Javascript 坚持这是用于 32 位有符号整数的格式)将 -2 存储为 11111111111111111111111111111110

      一切都如预期的那样。

      【讨论】:

      • 对,那为什么parseInt('11111111111111111111111111111110', 2)4294967294
      • 因为在那个场合,二进制表示无符号整数或64位有符号类型。
      【解决方案5】:

      这是补码算法。这相当于“磁带计数器”算术。录音机往往带有计数器(加法机可能是一个更好的类比,但当 2s 补码成为臀部时,它们已经过时了)。

      当你从 000 倒退 2 步时,你到达 998。所以 998 是磁带计数器对 -2 的 10s 补码算术表示:向前倒 2 步,再次到达 000。

      2s 补码就是这样。从 1111111111111111 向前旋转 1,你到达 0000000000000000,所以 1111111111111111 是 -1 的表示。风而不是从那里返回另一个 1,你会得到 1111111111111110,它是 -2 的表示。

      【讨论】:

        【解决方案6】:

        Numbers in JavaScript are floating point numbers,由 IEEE 754 标准存储和表示。

        但是,对于按位运算,操作数在内部被视为signed 32-bit integers represented by two's complement format

        所有位运算符的操作数都转换为有符号的 32 位 二进制补码格式的整数。二进制补码格式意味着 一个数字的负数(例如 5 对 -5)是所有 数字的位反转(按位不是数字,a.k.a. one' 数的补数)加一。

        负数的正数以同样的方式计算。因此我们有:

         1 = 00000000000000000000000000000001b
        ~1 = 11111111111111111111111111111110b
             11111111111111111111111111111110b = -2
        

        请注意,Number.toString() 不应该返回 base-2 的二进制补码表示。

        表达式(-2).toString(2) 产生-10,它是减号 (-) 后跟 2 (10) 的 base-2 表示。

        【讨论】:

          【解决方案7】:

          这是预期的行为。根据mdn:bitwise-not.

          你可能不明白的部分是 [11111111111111111111111111111110]₂ = [10]₂¹,如果表示为 有符号整数。前导 1s 可以任意多,并且它仍然是相同的数字,类似于无符号整数/小数中的前导 0s。

          ¹[10]₂ 指定 10 应解释为基数 2(二进制)

          【讨论】:

          • 你是说-10吗?指定你的基地可能也会减少混乱。
          猜你喜欢
          • 2022-04-12
          • 2014-07-04
          • 2013-08-17
          • 1970-01-01
          • 1970-01-01
          • 2016-05-14
          • 1970-01-01
          • 2019-06-06
          • 2017-08-08
          相关资源
          最近更新 更多