为什么按位 "not 1" 等于 -2?

Why does bitwise "not 1" equal -2?

假设我们有 1 并且以 2 为基数的这个数字是:

00000000000000000000000000000001

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

11111111111111111111111111111110

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

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

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

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

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

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

As @Derek , These 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; }

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

一切如预期。

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

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

中的前导 0

¹ [10]₂ 指定 10 应解释为基数 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  /

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

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

所以当你想在计算机中存储一个数字时,你必须编码它。对于非负数,这很简单,你只需要使用二进制表示。但是负数呢?

您可以使用称为 two's complement 的编码。在这种编码中,您必须决定每个数字将有多少位(例如 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,即使二进制值看起来相同。

这是二进制补码算法。这相当于 "tape counter" 算术。磁带录音机往往附有计数器(加法机可能是一个更好的类比,但当 2s 补码流行时它们已经过时了)。

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

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

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

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

The operands of all bitwise operators are converted to signed 32-bit integers in two's complement format. Two's complement format means that a number's negative counterpart (e.g. 5 vs. -5) is all the number's bits inverted (bitwise NOT of the number, a.k.a. ones' complement of the number) plus one.

负数对应的正数的计算方法相同。因此我们有:

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

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

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