Skip to content
Назад к блогу
Руководства

Побитовые операции: AND, OR, XOR, сдвиги и битовые маски

Освойте побитовые операции на практике: AND, OR, XOR, сдвиги, дополнение до двух, битовые маски и feature-флаги — с примерами на JS, Python, Go и C.

17 мин чтения

Побитовые операции на практике: AND, OR, XOR, сдвиги, маски

Вы открываете легаси-миграцию в PostgreSQL и видите permissions & 0b100. Коллега выкатывает систему feature-флагов, упаковывающую 32 булевых значения в одно целое. Калькулятор подсетей в Kubernetes выдаёт 192.168.1.0/24, и нужно извлечь сетевой адрес в коде. Три ситуации — один общий навык: побитовые операции.

Большинство разработчиков прикладного уровня никогда не тянутся к & или ^ в веб-приложении — пока вдруг не приходится. Это руководство проходит шесть побитовых операторов, дополнение до двух, девять паттернов, которые стоит запомнить, и языковые ловушки, которые укусят (особенно в JavaScript). Код на JS, Python, Go и C, каждый пример запускаемый.

Откройте наш конвертер систем счисления в соседней вкладке. Несколько разделов предложат ввести число и посмотреть, как меняется битовый паттерн.

Зачем побитовые операции в 2026 году

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

  • Row-level security в PostgreSQL использует битовую карту прав ACL (SELECT, INSERT, UPDATE, DELETE, …), упакованных в целое.
  • Linux capabilities заменяют старую модель «root или ничего» на 40+ битов разрешений, объединяемых через |.
  • Заголовки JWT-алгоритмов кодируют хеш-алгоритм в маленьком поле, где побитовое сравнение часто встречается на уровне библиотеки.
  • Snowflake, ULID и UUIDv7 упаковывают timestamp, machine ID и порядковый номер в одно 64-битное или 128-битное целое через сдвиги влево.
  • Redis BITCOUNT и BITOP напрямую раскрывают побитовые примитивы прикладному коду для оценки кардинальности и A/B-бакетинга.
  • Обработка изображений читает 32-битные RGBA-пиксели и извлекает каналы через & и >>.

Побитовые операции остаются O(1) на уровне инструкции CPU. Когда вы упаковываете 32 булевых в одно целое, вы экономите 31 байт памяти и (что важнее) можете проверить «хоть один из 32 флагов выставлен» одним тестом != 0.

Базы двоичной системы, которые нужны заранее

Это руководство предполагает, что вы знаете, как работает двоичная система. Освежить знания можно в нашем руководстве по конвертации систем счисления, после чего возвращайтесь.

Быстрая проверка словаря, прежде чем начнём:

  • Бит — это 0 или 1.
  • Полубайт (nibble) — это 4 бита (один шестнадцатеричный разряд).
  • Байт — это 8 бит.
  • Слово (word) — обычно 32 или 64 бита, зависит от CPU.

Целые в большинстве языков идут фиксированной ширины: 8, 16, 32, 64. Ширина важна для побитовых операций, потому что сдвиги могут вытолкнуть биты за край, а знаковый бит у знаковых целых сидит в крайней левой позиции.

Попробуйте прямо сейчас. Откройте конвертер систем счисления, введите 170 как десятичное и посмотрите на двоичный вывод. Должно получиться 10101010, чередующийся паттерн, к которому мы вернёмся ниже несколько раз.

Шесть побитовых операторов

Каждый мейнстрим-язык даёт одни и те же шесть операторов, иногда с лёгкими различиями синтаксиса. Символы &, |, ^, ~, <<, >> работают в JavaScript, Python, Go, Rust, C, C++, Java и C# без изменений. JavaScript добавляет один сверху: >>> — беззнаковый сдвиг вправо.

AND (&): фильтр битов

Выходной бит равен 1, только если оба входных бита равны 1.

ABA & B
000
010
100
111

Думайте про AND как про ворота: проходят только те биты, что выставлены в обоих операндах. Самое типичное применение — маскирование, чтобы оставить нужные биты и обнулить остальные.

// Извлечь младшие 4 бита (правый полубайт)
const value = 0b11010110;   // 214
const low4  = value & 0x0F; // 0b00000110 = 6

// Проверить, нечётное ли число
const isOdd = (n) => (n & 1) === 1;
isOdd(7);  // true
isOdd(42); // false
# То же на Python
value = 0b11010110
low4 = value & 0x0F  # 6

def is_odd(n):
    return (n & 1) == 1

OR (|): установщик битов

Выходной бит равен 1, если хотя бы один входной бит равен 1.

ABA | B
000
011
101
111

OR объединяет флаги. Если у вас READ = 1, WRITE = 2, EXECUTE = 4, то READ | WRITE равно 3 — оба разрешения включены.

const READ  = 0b001;
const WRITE = 0b010;
const EXEC  = 0b100;

const rw = READ | WRITE;  // 0b011 = 3
READ, WRITE, EXEC = 0b001, 0b010, 0b100
rw = READ | WRITE  # 3

XOR (^): переключатель битов

Выходной бит равен 1, если входные биты различаются.

ABA ^ B
000
011
101
110

У XOR три алгебраических свойства, на которых держатся самые элегантные трюки в computer science:

  • a ^ a = 0: что угодно XOR-нутое с собой даёт ноль.
  • a ^ 0 = a: XOR с нулём — тождество.
  • a ^ b ^ a = b: XOR — собственная инверсия.

Последнее свойство объясняет, почему XOR появляется в проверках чётности, поточных шифрах и пресловутом интервью-задании «найди единственное неповторяющееся число в массиве».

// Найти единственное уникальное число в массиве, где остальные встречаются дважды
const findUnique = (arr) => arr.reduce((a, b) => a ^ b, 0);
findUnique([4, 1, 2, 1, 2]);  // 4
from functools import reduce
from operator import xor
find_unique = lambda arr: reduce(xor, arr, 0)
find_unique([4, 1, 2, 1, 2])  # 4

NOT (~): инвертор битов

Унарный ~ переворачивает каждый бит: 0 становится 1, 1 — 0.

~0b00001111  // -16  (JavaScript приводит к 32-битному знаковому)
~5           // -6
~5  # -6
// Go использует ^ как унарный побитовый NOT — внимание
var x int8 = 5
fmt.Println(^x)  // -6

Результат ~5 равен -6 в любом мейнстрим-языке, и это удивляет начинающих. Причина — в дополнении до двух, которое мы разберём в следующем разделе. Пока запомните: ~x равно -(x + 1) в любом языке, использующем дополнение до двух для отрицательных (а это все они).

Сдвиг влево (<<): умножитель на степень двойки

x << n сдвигает каждый бит x влево на n позиций, заполняя нули справа. Математически это умножение на 2ⁿ.

1 << 0   // 1   (2^0)
1 << 1   // 2   (2^1)
1 << 3   // 8   (2^3)
1 << 10  // 1024 (2^10 = 1 KiB)

// Построение битовых флагов
const FLAG_ADMIN    = 1 << 0;
const FLAG_EDITOR   = 1 << 1;
const FLAG_REVIEWER = 1 << 2;

Удобство 1 << n в том, что выражение даёт число с одним установленным битом в позиции n. Этот бит становится флагом.

Осторожно с переполнением. В JavaScript 1 << 31 равно -2147483648 (а не 2147483648), потому что побитовые операторы JavaScript работают на 32-битных знаковых целых.

Сдвиг вправо (>> против >>>): деление или дополнение нулями?

Сдвиг вправо двигает биты вправо. Вопрос — чем заполняются освободившиеся крайние левые позиции.

  • >> (арифметический сдвиг вправо) сохраняет знаковый бит. Отрицательные числа остаются отрицательными.
  • >>> (логический, или беззнаковый, сдвиг вправо) заполняет нулями. Только в JavaScript есть специальный оператор для этого.
-8 >> 1   // -4   (знаковый бит сохранён)
-8 >>> 1  // 2147483644  (знаковый бит трактуется как обычный бит данных)

8 >> 1    // 4
8 >> 2    // 2

В C поведение >> для знаковых типов — арифметическое или логическое — implementation-defined. Большинство компиляторов делают арифметическое, но не полагайтесь на это без проверки. Go требует беззнаковых целых для величины сдвига и явно различает знаковые и беззнаковые типы. В Python нет >>>, потому что нет фиксированной ширины целых.

Дополнение до двух: как компьютеры представляют отрицательные

Если биты — это всего лишь 0 и 1, как закодировать -5? Ответ, на котором сошёлся мир в 1960-х, — дополнение до двух (two’s complement), и любой современный CPU использует именно его.

Наивный подход (зарезервировать один бит под знак) имеет две проблемы. Во-первых, появляются и +0, и -0, что неуклюже. Во-вторых, схемам сложения и вычитания приходится проверять знаковый бит — железо усложняется. Дополнение до двух решает обе проблемы.

Правило короткое:

  1. Возьмите положительное двоичное представление.
  2. Переверните каждый бит (это «дополнение до единицы»).
  3. Прибавьте 1.

Разбор примера, кодируем -5 в 8-битном дополнении до двух:

 5 в двоичной:        0000 0101
 переворот битов:     1111 1010   (это -6 в дополнении до двух!)
 +1:                  1111 1011   ← это -5

Проверьте через наш конвертер: введите 251 (десятичное) в конвертер систем счисления по основанию 10, и в двоичной части будет 11111011. В 8-битном знаковом контексте 11111011 — это -5. В 8-битном беззнаковом контексте тот же битовый паттерн — это 251. Биты идентичны, отличается интерпретация.

Это объясняет ранее замеченный сюрприз ~5 = -6. Побитовый NOT инвертирует биты — получается дополнение до единицы. Дополнение до двух — это дополнение до единицы плюс 1. Поэтому:

~x   = -(x + 1)        // тождество в любом языке с дополнением до двух
~5   = -6
~(-3) = 2

Для n-битных знаковых целых представимый диапазон — от -2ⁿ⁻¹ до 2ⁿ⁻¹ − 1. 8-битное знаковое покрывает -128…127. 32-битное знаковое — приблизительно от −2,1 миллиарда до +2,1 миллиарда.

Базовые паттерны битовой манипуляции

Эти девять паттернов покрывают, наверное, 95% битовой работы, которую вам когда-либо предстоит написать. Запомните их — узнаёте везде в системном коде.

Установить бит: x | (1 << n)

Включить бит n, оставив остальные неизменными.

let flags = 0b0100;
flags = flags | (1 << 0);  // 0b0101

Сбросить бит: x & ~(1 << n)

Выключить бит n, оставив остальные неизменными. ~(1 << n) — это маска, в которой выставлены все биты кроме бита n.

let flags = 0b0111;
flags = flags & ~(1 << 1);  // 0b0101

Переключить бит: x ^ (1 << n)

Перевернуть бит n независимо от текущего состояния.

let flags = 0b0100;
flags = flags ^ (1 << 2);  // 0b0000
flags = flags ^ (1 << 2);  // снова 0b0100

Проверить бит: (x >> n) & 1

Возвращает 1, если бит n выставлен, иначе 0. Эквивалентная форма: (x & (1 << n)) !== 0.

const flags = 0b0101;
const isBit2Set = (flags >> 2) & 1;  // 1

Изолировать самый младший установленный бит: x & -x

Даёт значение, в котором сохранён только самый правый бит 1 от x. Трюк работает потому, что -x в дополнении до двух равно ~x + 1, что переворачивает каждый бит вплоть до самого младшего установленного включительно.

const x = 0b10110100;
const lowest = x & -x;  // 0b00000100 = 4

Это базовый трюк внутри деревьев Фенвика (Binary Indexed Tree) для O(log n) префиксных сумм.

Подсчёт установленных битов (popcount)

Подсчёт количества единичных битов в целом. В большинстве языков сейчас есть нативная функция:

// JavaScript (BigInt или вручную)
const popcount = (n) => {
  let count = 0;
  while (n) { count += n & 1; n >>>= 1; }
  return count;
};
popcount(0b10110100);  // 4
# Python 3.10+
(0b10110100).bit_count()  # 4
// Go
import "math/bits"

bits.OnesCount(0b10110100) // 4

XOR-обмен без временной переменной

Классический фокус: обменять два целых без третьей переменной. Никогда не делайте так в продакшне (это медленнее временной переменной и ломается, если a и b указывают на одну ячейку памяти), но понимать стоит.

let a = 5, b = 9;
a = a ^ b;  // a = 5 ^ 9
b = a ^ b;  // b = (5 ^ 9) ^ 9 = 5
a = a ^ b;  // a = (5 ^ 9) ^ 5 = 9
// a = 9, b = 5

Проверка на степень двойки: (x & (x - 1)) === 0

У степени двойки ровно один установленный бит. Вычитание 1 переворачивает этот бит и выставляет все младшие. AND даёт ноль только для степеней двойки (и для 0 тоже, поэтому защитите условием x > 0).

const isPow2 = (x) => x > 0 && (x & (x - 1)) === 0;
isPow2(16);  // true
isPow2(17);  // false

Быстрая проверка нечётности: x & 1

В одних языках быстрее, чем x % 2, в других идентично после оптимизации компилятором. Имеет смысл в горячих циклах или там, где читаемость не важна.

const isOdd = (x) => (x & 1) === 1;

Битовые маски флагов в реальном коде

Описанные паттерны встречаются в production-коде каждый день. Четыре места, где их найдёте.

Feature-флаги в 32 булевых

Вместо структуры из 32 булевых полей упакуйте их в одно целое:

const FLAGS = {
  DARK_MODE:      1 << 0,
  NEW_NAV:        1 << 1,
  AI_SUGGESTIONS: 1 << 2,
  BETA_EDITOR:    1 << 3,
  // ... до 1 << 31
};

let userFlags = 0;
userFlags |= FLAGS.DARK_MODE | FLAGS.AI_SUGGESTIONS;  // включить

if (userFlags & FLAGS.AI_SUGGESTIONS) {
  showSuggestions();
}

userFlags &= ~FLAGS.DARK_MODE;  // выключить

Это хранит 32 булевых в 4 байтах и позволяет проверить любое подмножество одним AND. Базы данных любят этот паттерн, потому что вместо 32 колонок одна.

Права файлов в Unix

chmod 755 — это побитовая операция. Три восьмеричные цифры отображаются на три тройки бит:

7 = 111  (владелец: rwx)
5 = 101  (группа:   r-x)
5 = 101  (остальные:r-x)

Попробуйте: откройте конвертер систем счисления, выберите источник «восьмеричное», введите 755 и посмотрите на двоичный вывод 111101101. Это буквально то, как файловая система хранит поле прав.

Включить только «запись для группы»:

const perms = 0o755;
const withGroupWrite = perms | 0o020;  // 0o775

Маскирование подсетей IP

Дано 192.168.1.10/24, извлекаем сетевой адрес AND-ом с маской:

const ip      = 0xC0A8010A;  // 192.168.1.10
const mask    = 0xFFFFFF00;  // 255.255.255.0 (/24)
const network = ip & mask;   // 0xC0A80100 = 192.168.1.0

Упакованные ID: Snowflake

Twitter Snowflake упаковывает timestamp, machine ID и sequence в 64-битное целое:

┌─ 1 бит ──┬─── 41 бит ────┬─ 10 бит ──┬─ 12 бит ──┐
│  знак    │   timestamp   │ машина    │   seq     │
└──────────┴───────────────┴───────────┴───────────┘

Кодирование ID — два сдвига и два OR:

const id = (BigInt(timestamp) << 22n) |
           (BigInt(machineId) << 12n) |
            BigInt(sequence);

Декодирование — обратное: сдвиг вправо и маска. Полный разбор «когда выбрать Snowflake против ULID против UUIDv7» в нашем сравнении распределённых ID.

Кросс-языковые ловушки

JavaScript: ловушка приведения к 32 битам

Перед каждой побитовой операцией JavaScript приводит операнды к 32-битному знаковому целому, затем приводит результат обратно к Number. Любое значение выше 2³¹ − 1 = 2147483647 переполняется:

2147483647 | 0   // 2147483647   (всё ещё нормально)
2147483648 | 0   // -2147483648  (переполнение!)
4294967295 | 0   // -1           (все биты выставлены, интерпретация знаковая)

Для 64-битной работы используйте BigInt. У него независимые побитовые операторы без ограничения ширины:

(2n ** 40n) | 1n  // 1099511627777n

Баги приоритета операторов

Один из самых частых реальных побитовых багов:

// Баг: читается как (x & (1 == 0)), потому что == связывается крепче, чем &
if (x & 1 == 0) { /* ... */ }

// Правильно: расставить скобки
if ((x & 1) == 0) { /* ... */ }

Операторы сравнения связываются крепче побитовых AND/OR/XOR в C, JavaScript, Python, Go и большинстве потомков. В сомнении — ставьте скобки.

Сравнительная таблица языков

ЯзыкПриведение шириныОтрицательное >>Поддержка BigInt
JavaScriptПринудительно 32-битное знаковое; >>> беззнаковоеарифметическоеу BigInt свои операторы
PythonПроизвольная точность; ширины нетарифметическоенативно
GoСтрого; величина сдвига должна быть беззнаковойарифметическое для знаковых типовmath/big
C/C++Зависит от типа; int, unsigned и пр.implementation-defined для знаковыхвстроенной нет
RustСтрого; паника при переполнении в debugарифметическое для знаковых типовu128 или внешние крейты

Особенность Python: бесконечная ширина

У целых в Python нет фиксированной ширины, поэтому логика дополнения до двух «бесконечно» расширяется влево. Поэтому ~5 равно -6 (а не 250 или 65530): Python трактует результат как отрицательное целое, а не как фиксированный битовый паттерн. Если нужна семантика wrap-around, маскируйте явно:

# Имитация 8-битного NOT
(~5) & 0xFF  # 250

Реальная картина производительности в 2026 году

Расхожее мнение, что побитовые операции «всегда быстрее», в 2026 году верно лишь наполовину.

Очевидные перезаписи компиляторы делают сами. Современные оптимизаторы превращают x * 2 в x << 1 автоматически. Писать x << 1 в прикладном коде ради скорости — это карго-культовая оптимизация. Это не помогает и портит читаемость.

Где побитовый код реально выигрывает:

  • Горячие циклы в численном коде: popcount, подсчёт ведущих и хвостовых нулей, шахматные движки на bitboard.
  • Компактные структуры данных: фильтры Блума, roaring bitmap, деревья Фенвика.
  • Аппаратные регистры и memory-mapped I/O: embedded-код, ядра, прошивки.
  • Криптографические примитивы: AES, ChaCha20 и SHA построены из XOR, поворотов и сдвигов.
  • Сжатие и распаковка: код Хаффмана, RLE, упакованные целые.
  • Движки баз данных: bitmap-индексы, упакованные форматы колонок, например словарное кодирование Parquet.

Где не помогает: замена x % 2 на x & 1 в бизнес-логике, выполняющейся дважды на запрос. Ускорение неизмеримо, потеря читаемости реальна.

Единственный случай, где битовая манипуляция всегда выигрывает, — это объём памяти. Упаковка 32 флагов в один int экономит 31 байт против 32 булевых. На масштабе (миллионы записей пользователей, миллиарды событий) это разница между cache-friendly раскладкой и нагрузкой, которая колотит L2.

Краткий справочник

ОперацияОператорПримерРезультатТипичное применение
AND&0b1100 & 0b10100b1000Маскировать/извлекать биты
OR|0b1100 | 0b10100b1110Объединять флаги
XOR^0b1100 ^ 0b10100b0110Переключать / детектировать различие
NOT~~0b1100…11110011Инвертировать для маски
Сдвиг влево<<1 << 38Умножить на 2ⁿ
Сдвиг вправо>>16 >> 24Делить на 2ⁿ (знаковое)
Беззнаковый сдвиг вправо (JS)>>>-1 >>> 04294967295Трактовать как беззнаковое
Установить бит n|x | (1 << n)Включить бит
Сбросить бит n& ~x & ~(1 << n)Выключить бит
Переключить бит n^x ^ (1 << n)Перевернуть бит
Проверить бит n&(x >> n) & 10 или 1Тест бита
Самый младший выставленный бит& -x & -xИзолировать бит
Степень двойки&x > 0 && (x & (x-1)) == 0boolПроверка степени

FAQ

Чем отличаются логическое (&&) и побитовое (&) AND?

Логический AND работает с целыми булевыми значениями и короткозамкнут — false && expr никогда не вычислит expr. Побитовый AND работает на отдельных битах целых и всегда вычисляет обе стороны. Используйте && для условий, & — для манипуляций с битами.

Почему ~1 равно -2 в большинстве языков?

Побитовый NOT над 1 переворачивает каждый бит, давая дополнение до единицы. В представлении целых дополнением до двух переворот всех битов x даёт -(x + 1), поэтому ~1 = -2, ~0 = -1, а ~(-1) = 0. Тождество выполняется в JavaScript, Python, Go, C, Rust и любом другом языке, хранящем знаковые целые в дополнении до двух.

Действительно ли x << 1 быстрее, чем x * 2?

На практике нет. Любой современный компилятор распознаёт x * 2 и эмитит ту же инструкцию сдвига на машинном уровне, поэтому бенчмарки на x86 и ARM не показывают измеримой разницы. Используйте x * 2 для читаемости; держите << для случаев, когда вы намеренно мыслите в битах — построение битовой маски или упаковка структурированных ID.

Поддерживает ли JavaScript 64-битные побитовые операции?

JavaScript не поддерживает 64-битные побитовые операции стандартными &, |, ^, <<, >> — они принудительно приводят операнды к 32-битным знаковым целым. Для 64-битных и более крупных значений используйте литералы BigInt, например 1n << 40n, — у них произвольно-точные побитовые операции с собственным набором операторов.

Как эффективно посчитать число выставленных битов?

Используйте встроенное средство языка: bits.OnesCount в Go, Integer.bitCount в Java, .bit_count() в Python 3.10+, popcount-интринсики в C/C++. Они отображаются в одну инструкцию POPCNT на современных x86 и ARM.

Когда применять битовые маски флагов вместо структуры из булевых?

Применяйте битовые маски флагов, когда нужно компактно хранить много булевых (базы данных, сетевые протоколы, форматы файлов) или быстро тестировать комбинации одним AND вроде flags & REQUIRED_MASK. Предпочитайте структуру булевых, когда поля разных типов, нужен описательный отладочный вывод или читаемость важнее нескольких байт памяти.

Что происходит при сдвиге на величину больше ширины?

Не определено в C/C++. В JavaScript величина сдвига берётся mod 32, поэтому 1 << 32 — это 1, а не 0. В Python ширины нет, поэтому 1 << 100 — это просто большее целое. Не полагайтесь на поведение overshift; маскируйте величину сдвига сами при необходимости.

Почему ~5 в Python даёт -6, а не 2?

У целых в Python нет фиксированной ширины, поэтому дополнение до двух концептуально расширяется в бесконечность. ~5 = -(5 + 1) = -6, как и в любом другом языке с дополнением до двух. Если нужно 8-битное «инвертированное» значение 250, маскируйте: (~5) & 0xFF.

Безопасно ли XOR-шифрование?

Одноразовый блокнот (one-time pad) с действительно случайным ключом длиной с сообщение информационно-теоретически нерасшифруем. Повторное использование того же ключа на разных сообщениях — катастрофически небезопасно, а стандартное «XOR-шифрование» с коротким повторяющимся ключом тривиально вскрывается. Реальные шифры вроде AES и ChaCha20 используют XOR внутри, но как один шаг среди многих.

Как представить отрицательное число в дополнении до двух вручную?

Запишите положительное значение в двоичной системе нужной ширины, переверните каждый бит, прибавьте 1. Пример: -5 в 8 битах — 00000101 → переворот в 11111010 → плюс 1 → 11111011. Сверьтесь с нашим конвертером систем счисления: преобразуйте 251 (беззнаковая интерпретация 11111011) и убедитесь, что получается 11111011.

Связанные инструменты и материалы

Похожие статьи

Все статьи