Побитовые операции

Побитовые операции применяются только к целым числами и рассматривают их как набор битов.

Операции:

<< >>
~ & | ^
<<= >>= &= |= ^=

Обозначения

x, y, z = целые числа

w = 8 * sizeof(x) – количество бит в числе x

\(x_{w-1} x_{w-2} ... x_1, x_0\) – битовый вектор. \(x_{w-1}\) – старший разряд, \(x_0\) – младший разряд.

Побитовый сдвиг влево <<

y = x << a

_images/leftshift.png
\[y_i = x_{i-a}, if i \in [a, w-1]\]\[y_i = 0, if i \in [0, a-1]\]

x << a эквивалентно умножению x на \(2^a\)

Если a >= w, то результат сдвига не определён, разные компиляторы и процессоры могут выдавать разный ответ, так что a >= w лучше не пользоваться.

Представление целых чисел

Представление чисел без знака

\(x_{w-1} x_{w-2} ... x_1, x_0\) – битовый вектор. Если мы понимаем этот вектор как число без знака, то мы делаем это по формуле

\[x = \sum_{i=0}^{w-1} x_i 2^i\]
Бит \(x_{w-1}\) \(x_{w-2}\) ... \(x_1\) \(x_0\)
Вес \(2^{w-1}\) \(2^{w-2}\) ... \(2^1\) \(2^0\)

Из формулы видно, что \(x \in [0, 2^{32} - 1]\).

Представление чисел со знаком

Как мы помним, в представлении чисел со знаком в виде дополнения двойки, отличие есть лишь в знаке веса старшего бита:

Бит \(x_{w-1}\) \(x_{w-2}\) ... \(x_1\) \(x_0\)
Вес \(-2^{w-1}\) \(2^{w-2}\) ... \(2^1\) \(2^0\)

Из этих весов так же следует, что \(x \in [-2^{31}, 2^{31} - 1]\).

Побитовый сдвиг вправо

Мы вспомнили о различных представлениях целых чисел потому, что из-за разной интерпретации старшего бита в числах со знаком и без знака, существует две операции сдвига вправо: логическая и арифметическая.

Логический сдвиг вправо на а бит соответствует делению чисел без знака на \(2^a\). Схема работы сдвига показана на рисунке.

_images/rightshiftlogic.png

Арифметический сдвиг вправо на а бит соответствует делению чисел со знаком на \(2^a\) с округлением вниз. Схема работы показана на рисунке.

_images/rightshiftarithmetic.png

В языке Си:

  1. Для чисел без знака сдвиг вправо всегда логический.
  2. Для чисел со знаком тип сдвига зависит от компилятора поэтому для чисел со знаком сдвиг лучше не использовать. В этом случае поведение программы не будет меняться при перекомпиляции на другой платформе.

Побитовое логическое отрицание ~

y = ~x

Отрицание применяется к каждому биту отдельно:

\[y_i = not\ x_i\]
\(x_i\) \(not\ x_i\)
0 1
1 0
_images/bitneg.png

Тут следует напомнить, что логическое отрицание !x работает иначе и зануляет все биты результата, если в x есть хотя бы один ненулевой бит, или зануляет все кроме младшего, а младший делает единичным, если все биты x нули.

Побитовое логическое “и”

z = x & y; // Бинарный &

Логическое “и” применяется к каждой паре бит отдельно:

\[z_i = x_i\ AND\ y_i\]
\(x_i\) \(y_i\) \(x_i\ AND\ y_i\)
0 0 0
0 1 0
1 0 0
1 1 1
_images/bitand.png

Примечание. Когда мы используем побитовое и, и одним из операторов является переменная, а другим – константа, такая константа называется маской.

_images/bitandmask.png

Внимание! Побитовое “и” нельзя использовать в качестве логического. Это не эквивалентные операции.

2 && 1  // 1
2 & 1   // 0

Побитовое логическое “или”

z = x | y;

\[z_i = x_i\ OR\ y_i\]
\(x_i\) \(y_i\) \(x_i\ OR\ y_i\)
0 0 0
0 1 1
1 0 1
1 1 1
_images/bitor.png

Обратите так же внимание на неэквивалентность | и ||:

2 || 1  // 1
2 | 1   // 3

Побитовое логическое исключающее “или”

z = x ^ y;

\[z_i = x_i\ XOR\ y_i\]
\(x_i\) \(y_i\) \(x_i\ XOR\ y_i\)
0 0 0
0 1 1
1 0 1
1 1 0
_images/bitxor.png

Побитовое логическое или включено в синтаксис языка си, так как оно реализовано как быстрая операция почти на всех процессорах и оно обладает замечательным свойством

\[x\ XOR\ y \ XOR\ y = x \quad \forall x, y\]

Это свойство особенно важно для шифрования. Если x – секретное число, которое надо зашифровать, а y – секретное число, неизвестное взломщику, то

z = x ^ y;  // Зашифрованное x
x1 = z ^ y;  // Расшифрованное x

Приоритеты побитовых операций

TODO: картинка

Побитовые операции в порядке убывания приоритета:

~
<< >>
&
^
|