&, |, ^, ~ и сдвиги работают с каждым разрядом двоичного представления отдельно. Похожи на логические операции внешне, но это другая история: короткого замыкания здесь нет.

Как считать

Возьмём 6 = 110₂ и 3 = 011₂:

  110     110       110
& 011   | 011     ^ 011
-----   -----     -----
  010     111       101

& оставляет единицу там, где она есть в обоих операндах, | — где есть хотя бы в одном, ^ (исключающее ИЛИ) — где биты различаются. Результаты: 2, 7 и 5.

6 & 3    // 2
6 | 3    // 7
6 ^ 3    // 5

Побитовое отрицание ~ меняет все биты значения после продвижения, а не только те три разряда, которые нарисованы:

~6      // -7
~0U     // максимальное значение unsigned int

Для знакового числа результат читается через дополнительный код, поэтому ~6 и даёт −7. Смотреть на ~ как на «перевернуть три бита» — верный способ получить неожиданный ответ.

Сдвиги

6U << 1    // 12
6U >> 1    // 3
7U >> 1    // 3

В этих примерах сдвиг влево эквивалентен умножению на два, вправо — целому делению на два. Но делать отсюда универсальную замену арифметики сдвигами нельзя: для отрицательных чисел поведение расходится.

В C++20 сдвиг отрицательного знакового числа вправо округляет вниз, а целочисленное деление округляет к нулю:

-7 >> 1    // -4
-7 / 2     // -3

Количество позиций должно быть неотрицательным и меньше ширины типа левого операнда после продвижения. Нарушение — Неопределённое поведение. Сдвиг 32-битного значения на 32 позиции не даёт ноль, он даёт UB.

Главное отличие от логических операций

& и | вычисляют оба операнда всегда. Короткое замыкание есть только у && и ||:

bool ok  = y != 0 && x / y > 2;   // деления нет, если y == 0
bool bad = y != 0 &  x / y > 2;   // деление выполняется всегда → UB

Замена одного символа превращает корректный код в программу с неопределённым поведением. Подробнее — Логические операции и короткое замыкание.

Тонкости

  • Тип результата определяется не обычной схемой приведения к общему типу: у сдвигов тип берётся от левого операнда после продвижения, правый на него не влияет. Для остальных побитовых операций работают обычные Арифметические преобразования типов.
  • Малые типы продвигаются и здесь: ~ над char действует на все биты int.
  • Для битовых манипуляций предпочтительны беззнаковые типы — там определены и переполнение, и сдвиг влево через старший бит. См. Беззнаковые типы.
  • Для чисел с плавающей точкой побитовые операции не определены вовсе.
  • В std::cout << x те же символы << означают вывод, а не сдвиг. Смысл операции задают операнды, а не значок.

Частые ошибки

  • Писать & вместо && в защитном условии.
  • Считать ~x «инверсией нарисованных битов» и забывать про ширину типа.
  • Сдвигать на количество позиций, равное ширине типа или больше.
  • Заменять x / 2 на x >> 1 для знаковых величин, которые бывают отрицательными.