Битовые маски и сдвиги

Как уместить десяток настроек «вкл/выкл» в одно число и доставать их побитовыми операциями — приём, на котором держатся права доступа, флаги и сетевые протоколы.

Битовая маска — это число, в котором каждый бит отдельно означает «да/нет» для одной настройки; операции И, ИЛИ и сдвиги позволяют включать, выключать и проверять эти флаги по одному.

Представь настройки программы: «звук включён?», «показывать уведомления?», «тёмная тема?». Каждый ответ — это да или нет, то есть один бит. Хранить десяток таких флагов отдельными переменными расточительно. Гораздо компактнее упаковать их в одно целое число, где каждый бит отвечает за свой флаг. Это и есть битовая маска — приём, который ты постоянно встречаешь в правах доступа файлов, графике и сетевых протоколах.

Биты как флаги

Договоримся: бит номер 0 (самый младший) — это «звук», бит 1 — «уведомления», бит 2 — «тёмная тема». Тогда значение каждого флага — это степень двойки:

$$ \text{звук} = 2^0 = 1, \quad \text{уведомления} = 2^1 = 2, \quad \text{тема} = 2^2 = 4 $$

Почему именно степени двойки? Потому что в двоичной записи у каждой из них горит ровно один бит и на разных позициях: 001, 010, 100. Это гарантирует, что флаги не пересекаются. Если включить звук и тёмную тему, складываем (или объединяем через ИЛИ): $1 + 4 = 5$, в двоичном 101 — горят биты 0 и 2, ровно нужные.

Три базовые операции

Чтобы управлять флагами, нужны три побитовые операции. Они работают над каждой парой битов независимо.

  • ИЛИ (|)включить флаг. Бит результата равен 1, если хотя бы один из исходных битов 1. Чтобы добавить флаг, делаем маска | флаг.
  • И (&)проверить флаг. Бит результата равен 1, только если оба бита 1. Чтобы узнать, включён ли флаг: маска & флаг даёт ненулевой результат, если бит горит.
  • Сдвиг влево (<<)построить флаг. 1 << n ставит единицу на позицию n, то есть даёт $2^n$. Это удобный способ записать «флаг номер n» без степеней вручную.

Связь сдвига со степенью двойки прямая:

$$ 1 \ll n = 2^n $$

Например, $1 \ll 3 = 8$, потому что единица переехала на 3 позиции влево: из 0001 в 1000.

Упаковка настроек в Python

Соберём всё вместе. Включим звук и тёмную тему, проверим каждый флаг и затем выключим звук.

SOUND = 1 << 0   # 1, бит 0
NOTIFY = 1 << 1  # 2, бит 1
DARK = 1 << 2    # 4, бит 2

# включаем звук и тёмную тему через ИЛИ
settings = SOUND | DARK
print("Маска:", settings, "=", bin(settings))

# проверяем каждый флаг через И
print("Звук включён:", bool(settings & SOUND))
print("Уведомления:", bool(settings & NOTIFY))
print("Тёмная тема:", bool(settings & DARK))

# выключаем звук: И с инверсией флага
settings = settings & ~SOUND
print("После выключения звука:", settings, "=", bin(settings))

Вывод:

Маска: 5 = 0b101
Звук включён: True
Уведомления: False
Тёмная тема: True
После выключения звука: 4 = 0b100

Разберём по шагам. SOUND | DARK — это 001 | 100 = 101, то есть 5. Проверка settings & SOUND даёт 101 & 001 = 001 (ненулевое, флаг есть), а settings & NOTIFY даёт 101 & 010 = 000 (ноль, флага нет). Выключение звука — это И с инверсией: ~SOUND гасит нулём только бит 0, остальные оставляет, поэтому 101 превращается в 100.

Как это работает: маска прав доступа

Классический пример из реальной жизни — права доступа к файлам в Unix. Для каждого файла есть три права: чтение (read), запись (write), исполнение (execute). Их кодируют тремя битами:

$$ r = 4 = 2^2, \quad w = 2 = 2^1, \quad x = 1 = 2^0 $$

Команда chmod 6 означает права $6 = 4 + 2 = r + w$: читать и писать можно, исполнять — нет. А знаменитое chmod 7 — это $4 + 2 + 1 = 7$, все три права сразу (111). Вот таблица всех комбинаций одной группы прав:

ЦифраБиты rwxЧто разрешено
0000ничего
4100только чтение
6110чтение + запись
5101чтение + исполнение
7111чтение + запись + исполнение

Когда система проверяет, можно ли тебе писать в файл, она делает ровно права & w — то же побитовое И, что и в нашем примере с настройками. Один и тот же приём работает и для прав, и для флагов окна, и для битовых полей в сетевом пакете.

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

  • Путать & и &&. Одиночный & — побитовое И (работает с битами), двойной && — логическое И (работает с «истина/ложь»). Для масок нужен именно одиночный.
  • Складывать флаги вместо ИЛИ при возможном повторе. Если флаг уже включён, + добавит его второй раз и сломает соседние биты, а | повторное включение проигнорирует. Для масок безопаснее |.
  • Брать не степени двойки. Если назначить флагу значение 3 (011), он займёт сразу два бита и пересечётся с другими флагами. Значения флагов обязаны быть $1, 2, 4, 8, \ldots$
  • Забывать, что проверка даёт число, а не «истину». settings & DARK возвращает 4, а не True. Чтобы получить булево, оборачивай в bool(...) или сравнивай с нулём.

Итоги

  • Битовая маска упаковывает много флагов «да/нет» в одно число, по биту на флаг.
  • Значение каждого флага — степень двойки $2^n$, чтобы биты не пересекались; удобно строить через $1 \ll n$.
  • ИЛИ (|) включает флаг, И (&) проверяет, И с инверсией (& ~) выключает.
  • Права доступа rwx — это та же маска: $r=4,\ w=2,\ x=1$, а chmod 7 = все права.
  • Тот же приём лежит под флагами окон, графикой и полями сетевых пакетов.
Проверьте себя
1. Какая побитовая операция включает флаг в маске, не задевая остальные биты?
AИ (&), потому что оставляет только общие биты
BИЛИ (|), потому что ставит бит в 1, остальные не трогает
CСдвиг вправо (>>)
DВычитание, потому что убирает лишнее
2. Чему равно выражение 1 << 3?
A3
B6
C8
D16