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

31

假设我们有数字1,它在二进制下的表示为:

00000000000000000000000000000001

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

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

As far as I know, the solution is to use the ~ (bitwise NOT operator) to flip all bits, but the result of ~1 is -2:

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

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


42
“11111111111111111111111111111110” 相当于 “-2”。 - zerkms
1
11111111111111111111111111111111-1 > ~0 === -1 - Cerbrus
2
@AfshinMehrabani 是的,因为 parseInt 的第一个参数必须是一个字符串。parseInt('11111111111111111111111111111110', 2) | 0 - zerkms
3
所有位运算符的操作数都会被转换为有符号32位整数,使用二进制补码格式。二进制补码格式意味着一个数字的负数对应值(例如5和-5)是该数字所有位的按位取反(即按位非运算,也称为一补数)再加上1。 - Andreas
5
在纸上用手将它加二,你会发现结果为零。而使得加二后结果为零的数字应被称为负二。 - harold
显示剩余9条评论
7个回答

63

在1和-2之间有两个整数:0-1

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

正如你所看到的,~1等于-2并不奇怪,因为~0等于-1

正如@Derek在这里解释的那样,这些按位运算符将其操作数视为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; }


3
好的,明白了。答案是:如果 11111111111111111111111111111110 表示的是负数 -2,那么为什么执行 parseInt('11111111111111111111111111111110', 2) 后返回的是 4294967294 - Afshin Mehrabani
FYI 在 JavaScript 中,数字以有符号的二进制补码表示。 - TaoPR
4
按位运算符只会将操作数作为32位整数处理。因此,当使用~时,你得到的是-2,但解析时得到的是4294967294,这并不奇怪。 - Derek 朕會功夫
2
@AfshinMehrabani 这就是有符号整数和无符号整数之间的区别。 - Florian Wendelborn
3
在上面每一行中,数学上应该写成“在2的补码下”,而不是“在二进制下”。-2在二进制下是-10,在计算机中必须有一种存储符号的方法。此外,还有无符号数据类型,仍然是基于2进制,但当然不是2的补码。 - Todd Wilcox
2
@ToddWilcox:从数学上讲,这不仅仅是二进制补码。要从任何数字中减去一个数字,从右侧开始扫描,将零更改为一,直到遇到一个一。将其更改为零并停止。从零减去一个数字将导致所有零变成一。二进制补码意味着数字的MSB应该向左扩展到所有位。一的补码意味着它应该向左和向右扩展(请注意,在二进制中,0.11111111 ...(无限)与1任意接近,因此....11111111.11111111 .....(两个方向无限)任意接近于0)。 - supercat

20
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  /

16
在计算机科学中,一切都是关于解释的。对于计算机来说,一切都是可以用许多方式解释的比特序列。例如,0100001 可以是数字33或者是 !(这就是 ASCII 将该比特序列映射成的方式)。
对于计算机来说,一切都是比特序列,无论你将其视为数字、字母、文本、Word 文档、屏幕上的像素、显示的图像还是硬盘驱动器上的 JPG 文件。如果你知道如何解释这个比特序列,它可能会被转化为人类可读的有意义的东西,但在 RAM 和 CPU 中只有比特。
因此,当你想要在计算机中存储一个数字时,你必须对其进行编码。对于非负数来说,这很简单,你只需要使用二进制表示法。但负数怎么办呢?

您可以使用一种称为二进制补码的编码方式。在这种编码方式中,您需要决定每个数字将有多少位(例如8位)。最高有效位被保留作为符号位。如果它是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

再次举例,最高有效位丢失了,我们得到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。


6

一个使用2进制补码表示的32位有符号整数(Javascript中规定这是32位有符号整数的格式),将-2存储为11111111111111111111111111111110

所有结果都与预期相同。


对了,那为什么 parseInt('11111111111111111111111111111110', 2) 的结果是 4294967294 呢? - Afshin Mehrabani
4
那个时候二进制表示的是一个无符号整数,或者是一个64位有符号类型。 - Bathsheba

5
这是二进制补码算术。它相当于“磁带计数器”算术。磁带录音机通常会附带计数器(加法机可能是更好的类比,但在2s补码流行时已经过时了)。
当你从000往回倒2步时,你到达998。所以998是磁带计数器的10进制补码表示为-2:向前走2步,再次到达000。
2s补码就像这样。从1111111111111111向前走1步,你到达0000000000000000,因此1111111111111111是-1的表示。反过来再往后退1步,你得到1111111111111110,这就是-2的表示。

4
这是期望的行为。根据 mdn:bitwise-not
你可能不理解的部分是,如果表示为有符号整数[11111111111111111111111111111110]₂=[10]₂¹。前导的1可以有很多个,但仍然是相同的数字,类似于无符号整数/十进制中的前导0s。
¹ [10]₂指定10应解释为基数2(二进制)

1
你是不是想说-10?指定基数可能会使这更清晰。 - Samuel Edwin Ward

4

JavaScript中的数字是浮点数,由IEEE 754标准存储和表示。

然而,对于位运算,操作数在内部被视为以二进制补码格式表示的有符号32位整数:

所有位运算符的操作数都会被转换为以二进制补码格式表示的有符号32位整数。二进制补码格式意味着一个数的负数副本(例如5与-5)是该数的所有位取反(该数的按位NOT,也称为该数的一次补码)加上1。

一个负数的正数副本的计算方式相同。因此我们有:

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

请注意,Number.toString() 不应返回二进制补码表示法。
表达式 (-2).toString(2) 的结果为 -10,其中 - 是减号,后面跟着数字 2 的二进制表示 10

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接