Почему побитовое "не 1" равно -2?
Предположим, у нас есть 1
и это число в базе 2:
00000000000000000000000000000001
Теперь я хочу перевернуть все биты, чтобы получить следующий результат:
11111111111111111111111111111110
Насколько я знаю, решение состоит в том, чтобы использовать ~
(побитовый оператор НЕ), чтобы перевернуть все биты, но результат ~1
является -2
:
console.log(~1); //-2
console.log((~1).toString(2)); //-10 (binary representation)
Почему я получаю этот странный результат?
7 ответов
Есть 2 целых числа между 1
а также -2
: 0
а также -1
1
в двоичном есть 00000000000000000000000000000001
0
в двоичном есть 00000000000000000000000000000000
-1
в двоичном есть 11111111111111111111111111111111
-2
в двоичном есть 11111111111111111111111111111110
("двоичный" является дополнением 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; }
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 на жестком диске. Если вы знаете, как интерпретировать эту битовую последовательность, она может стать чем-то значимым для человека, но в ОЗУ и ЦП есть только биты.
Поэтому, когда вы хотите сохранить число на компьютере, вы должны его кодировать. Для неотрицательных чисел это довольно просто, вам просто нужно использовать двоичное представление. Но как насчет отрицательных чисел?
Вы можете использовать кодировку, называемую дополнением до двух. В этой кодировке вы должны решить, сколько бит будет иметь каждое число (например, 8 бит). Самый значимый бит зарезервирован как знаковый бит. Если это 0
тогда число должно быть интерпретировано как неотрицательное, иначе оно отрицательное. Другие 7 бит содержат действительное число.
00000000
означает ноль, как и для чисел без знака. 00000001
это один, 00000010
два и так далее. Наибольшее положительное число, которое вы можете сохранить на 8 битах в дополнении до двух, составляет 127 (01111111
).
Следующее двоичное число (10000000
) -128. Это может показаться странным, но через секунду я объясню, почему это имеет смысл. 10000001
-127, 10000010
-126 и так далее. 11111111
это -1.
Почему мы используем такую странную кодировку? Из-за его интересных свойств. В частности, при выполнении сложения и вычитания процессор не должен знать, что это число со знаком, хранящееся как дополнение к двум. Он может интерпретировать оба числа как беззнаковые, сложить их вместе, и результат будет правильным.
Давайте попробуем это: -5 + 5. -5 это 11111011
, 5
является 00000101
,
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, хотя двоичное значение, по-видимому, одинаково.
32-битное целое число со знаком, дополненное 2 (Javascript настаивает, что это формат, используемый для 32-битного целого числа со знаком), будет хранить -2 как 11111111111111111111111111111110
Так что все как положено.
Это арифметика с двумя дополнениями. Что является эквивалентом арифметики "счетчик ленты". К магнитофонам, как правило, прикрепляли счетчики (добавление машин, вероятно, было бы еще лучшей аналогией, но они уже устарели, когда дополнение 2s стало модным).
Если вы вернетесь назад на 2 шага от 000, вы получите 998. Таким образом, 998 - это арифметическое представление счетчика ленты с 10-ой единицей для -2: поверните вперед на 2 шага, снова достигните 000.
2s дополнение просто так. Сверните вперед 1 от 1111111111111111, и вы получите 0000000000000000, поэтому 1111111111111111 является представлением -1. Вместо этого переверните еще одну 1, и вы получите 1111111111111110, который затем является представлением -2.
Это ожидаемое поведение. По МДН: поразрядно - нет.
Часть, которую вы, вероятно, не понимаете, состоит в том, что [11111111111111111111111111111110]₂ = [10]₂
¹, если выражено как целое число со знаком. Ведущий 1
может быть столько, сколько вы хотите, и это все тот же номер, похожий на ведущий 0
s в беззнаковых целых / десятичных.
¹ [10]₂
указывает, что 10
следует интерпретировать как основание 2 (двоичное)
Числа в JavaScript - это числа с плавающей запятой, которые хранятся и представлены стандартом IEEE 754.
Однако для побитовых операций операнды внутренне обрабатываются как 32-разрядные целые числа со знаком, представленные в формате дополнения до двух:
Операнды всех побитовых операторов преобразуются в 32-разрядные целые числа со знаком в формате дополнения до двух. Формат дополнения к двум означает, что отрицательный аналог числа (например, 5 против -5) - это все инвертированные биты числа (поразрядно НЕ числа, то есть дополнения числа) плюс один.
Положительное число отрицательного числа рассчитывается так же. Таким образом, мы имеем:
1 = 00000000000000000000000000000001b
~1 = 11111111111111111111111111111110b
11111111111111111111111111111110b = -2
Обратите внимание, что Number.toString()
не должен возвращать представление дополнения двух для base-2.
Выражение (-2).toString(2)
доходность -10
который является знаком минус (-
) с последующим представлением базы-2 2
(10
).