Услуги Портфолио Контакты БлогНовости +7 (800) 511‑32‑27 Обсудить проект
Статьи

Хитрости и приемы с битовыми операциями

16 февраля 2023·4 мин чтения
Логотип YuSMP Group
Автор материалаРедакция компании YuSMP GroupБлог YuSMP Group
Профиль редакции
Хитрости и приемы с битовыми операциями
TL;DR — кратко о главном
  • Битовые операции — AND (&), OR (|), XOR (^), NOT (~) и сдвиги <<, >> — работают напрямую с двоичным представлением числа и выполняются за одну машинную инструкцию, поэтому в разы быстрее обычной арифметики.
  • AND и OR используют для битовых масок и флагов, XOR — для простого шифрования и переключения бита, NOT — для инверсии, сдвиги — для быстрого умножения и деления на степени двойки.
  • Логика одинакова в C++, JavaScript, Python и PHP — отличаются символы части операторов и обработка знаковых чисел.
  • Ниже — таблицы истинности, код на нескольких языках, практическое применение и типичные ошибки: например, путаница & с && или использование >> вместо >>> для беззнакового сдвига в JS.

Битовые операции имеют множество применений. Криптография, компьютерная графика, хэш-функции, цифровая обработка изображений, алгоритмы сжатия и сетевые протоколы — вот лишь некоторые примеры, где такие операции чрезвычайно полезны.

В блоге студии web-разработки YuSMP Group рассмотрели, как работают базовые битовые операторы AND, OR, XOR, NOT и сдвиги, и собрали несколько приемов по манипулированию битами. Такие низкоуровневые оптимизации наша команда использует и в промышленной разработке ПО под ключ — от бэкенда до мобильных и embedded-модулей.

Что такое битовые операции и зачем они нужны

Бит — наименьшая единица данных: он принимает только одно из двух значений, 0 или 1. Битовая операция работает не с числом целиком, а с каждым его битом по отдельности, обрабатывая двоичное представление числа напрямую. В отличие от логических операторов && и ||, которые сравнивают булевы значения true/false и используют короткое замыкание, битовые операторы &, |, ^, ~, <<, >> применяются одновременно ко всем битам числа и возвращают число, а не булево значение.

Именно поэтому битовые операции выполняются за одну-две машинные инструкции процессора и в разы быстрее обычной арифметики или условных конструкций — это классический приём оптимизации в системном программировании, компьютерной графике, играх, сетевых протоколах и алгоритмах сжатия.

Двоичные данные и логические схемы, обрабатывающие биты 0 и 1

Основные битовые операторы: AND, OR, XOR, NOT

К основным битовым операторам относят три парные логические операции и одну унарную — инверсию. Каждая работает по фиксированному правилу, которое проще всего показать таблицей истинности: на входе — значения двух битов (0 или 1), на выходе — результат операции.

И (AND, &)

aba & b
000
010
100
111

Оператор И (в большинстве языков — &) возвращает 1 только тогда, когда оба бита равны 1 — во всех остальных случаях результат 0. Пример на JavaScript:

const a = 0b1010; // 10 const b = 0b0110; // 6 console.log(a & b); // 0b0010 = 2

На практике AND используют, чтобы «обнулить» ненужные биты числа — например, проверить, включён ли конкретный флаг: flags & MASK.

ИЛИ (OR, |)

aba | b
000
011
101
111

Оператор ИЛИ возвращает 1, если хотя бы один из битов равен 1. Его используют, чтобы включить (установить) бит, не трогая остальные: flags = flags | MASK. Пример на Python:

a = 0b1010 b = 0b0110 print(bin(a | b)) # 0b1110

Исключающее ИЛИ (XOR, ^)

aba ^ b
000
011
101
110

XOR возвращает 1, если биты различаются, и 0 — если совпадают. Два свойства XOR делают его особенно полезным: x ^ x = 0 (число, сложенное само с собой по XOR, даёт ноль) и x ^ 0 = x (XOR с нулём не меняет число). На этом строится обмен значений двух переменных без временной переменной и простое симметричное шифрование (см. раздел «Где применяют» ниже). Пример на C++:

int a = 10, b = 6; int c = a ^ b; // 12

НЕ (NOT, ~)

Унарный оператор NOT (~) инвертирует каждый бит числа: 0 становится 1, а 1 — 0. Важная деталь: в языках со знаковыми целыми числами (C++, Java, JavaScript) результат ~x равен -x - 1, потому что отрицательные числа хранятся в дополнительном коде — это частая причина ошибок у новичков, которые ожидают увидеть просто «перевёрнутые биты» в десятичном виде. Например, ~5 в 32-битном int даёт -6, а не положительное число с перевёрнутыми битами.

Побитовые сдвиги (<<, >>, >>>)

Операторы сдвига перемещают все биты числа влево или вправо на заданное количество позиций:

  • << (сдвиг влево) — дописывает нули справа; каждый сдвиг на 1 эквивалентен умножению на 2.
  • >> (арифметический сдвиг вправо) — отбрасывает младшие биты, а освобождающиеся старшие биты заполняются знаковым битом (сохраняет знак числа); сдвиг на 1 эквивалентен целочисленному делению на 2.
  • >>> (логический сдвиг вправо, есть в Java и JavaScript) — заполняет освобождающиеся биты нулями независимо от знака, поэтому у отрицательных чисел даёт совсем другой результат, чем >>.

Пример на JavaScript:

let x = 5; console.log(x << 2); // 20 (5 * 4) console.log(-8 >> 1); // -4 (знак сохранён) console.log(-8 >>> 1); // 2147483644 (знак потерян)

Сдвиги на степень двойки быстрее универсального умножения и деления и в компилируемых, и в интерпретируемых языках, поэтому их до сих пор используют в высоконагруженных алгоритмах и системном коде — хотя современные компиляторы часто сами заменяют x * 2 на x << 1, если это безопасно.

Где применяют битовые операции на практике

Помимо академического интереса, битовые операции решают конкретные инженерные задачи.

  • Битовые маски и флаги. Вместо десятка булевых полей в объекте можно хранить набор независимых флагов в одном числе: PERMISSION_READ = 1, PERMISSION_WRITE = 2, PERMISSION_EXEC = 4. Проверка — через permissions & PERMISSION_WRITE, включение — permissions |= PERMISSION_WRITE, выключение — permissions &= ~PERMISSION_WRITE. Такой подход экономит память и применяется в правах доступа, конфигурациях и низкоуровневых API операционных систем.
  • Простое XOR-шифрование. Если зашифровать сообщение операцией XOR с ключом, повторное применение того же XOR с тем же ключом полностью восстанавливает исходные данные — следствие свойства x ^ x = 0. Промышленным шифрованием такой метод не является (ключ легко подобрать при известном открытом тексте), но он лежит в основе более сложных потоковых шифров и часто встречается в учебных задачах и лёгкой обфускации данных.
  • Быстрая проверка чётности числа. n & 1 мгновенно определяет чётность числа (0 — чётное, 1 — нечётное), не прибегая к операции деления с остатком n % 2.
  • Упаковка и распаковка цвета RGB. Цвет пикселя часто хранят одним 32-битным числом: каждый канал (R, G, B, alpha) занимает свои 8 бит. Чтобы получить канал, число сдвигают и накладывают маску: red = (color >> 16) & 0xFF. Это применяется в графических библиотеках, обработке изображений и играх.
  • Оптимизация в сетевых протоколах и сжатии. Заголовки протоколов (например, IP, TCP) и форматы сжатия данных плотно упаковывают служебную информацию в отдельные биты байта, и разбор такого заголовка — это всегда набор сдвигов и масок.
Каналы цвета RGB в виде сетки пикселей, упакованные в одно число

Частые ошибки при работе с битовыми операциями

  • Путают & и &&. & — побитовый оператор, работает со всеми битами числа; && — логическое И, работает с булевыми значениями и использует короткое замыкание. if (a & b) и if (a && b) в общем случае дают разный результат.
  • Забывают про приоритет операторов. Во многих языках &, | и ^ имеют более низкий приоритет, чем операторы сравнения — выражение flags & MASK == 0 без скобок почти всегда работает не так, как ожидается, потому что сначала выполнится MASK == 0.
  • Путают >> и >>> в JavaScript и Java. Для отрицательных чисел это два разных результата: >> сохраняет знак, >>> — нет.
  • Ожидают от ~x «человеческого» результата. ~x в знаковой арифметике равно -x - 1, а не положительному числу с перевёрнутыми битами.
  • Переполнение при сдвиге влево. Сдвиг за пределы разрядности типа (например, 1 << 32 для 32-битного int) даёт неопределённое или платформозависимое поведение — частый источник трудноуловимых багов в C и C++.

Сводная таблица битовых операторов по языкам

ОперацияPythonJavaScriptC++ / JavaPHP
И (AND)&&&&
ИЛИ (OR)||||
Искл. ИЛИ (XOR)^^^^
НЕ (NOT)~~~~
Сдвиг влево<<<<<<<<
Сдвиг вправо (знаковый)>>>>>>>>
Сдвиг вправо (беззнаковый)— (не поддерживается)>>>Java: >>>, в C++ нет— (не поддерживается)

В Python нет отдельного беззнакового сдвига и оператора >>>, потому что целые числа в Python не имеют фиксированной разрядности и хранятся с произвольной точностью.

Смежные приёмы: деление, остаток и возведение в степень

Рядом с чисто битовыми AND/OR/XOR/NOT в этом же классе оптимизаций часто разбирают смежные арифметические приёмы — деление с остатком и возведение в степень, — которые в соревновательном программировании и повседневном коде используют вместе с битовыми операциями для ускорения вычислений.

Для нахождения частного и остатка (C++)

В этом примере вы научитесь находить частное и остаток от деления С++.

#include <iostream> using namespace std; int main() { int divisor, dividend, quotient, remainder; cout << "Enter dividend: "; cin >> dividend; cout << "Enter divisor: "; cin >> divisor; quotient = dividend / divisor; remainder = dividend % divisor; cout << "Quotient = " << quotient << endl; cout << "Remainder = " << remainder; return 0; }

Чтобы вычислить частное и остаток, делитель и делимое должны быть целыми числами.

Оператор вычисляет частное (между вещественными или целочисленными переменными).

Оператор модуля % вычисляет остаток при делении одного целого числа на другое (нельзя использовать для переменных с плавающей запятой).

Найти остаток от деления (js)

Оператор ( %) возвращает javascript остаток от деления и всегда принимает знак делимого.

Описание

Для операции n % d, n называется делимым и называется делителем. Операция возвращает значение NaN, если один из операндов равен NaN, n равен ± бесконечности или d равен ± 0. В противном случае, если d± бесконечность или n± 0, n возвращается.

Когда оба операнда не равны нулю и конечны, остаток r вычисляется как r := n - d * q где q целое число, r имеющее тот же знак, что и n, но максимально близкое к 0.

Примеры С положительным дивидендом 13 % 5; // 3 1 % -2; // 1 1 % 2; // 1 2 % 3; // 2 5.5 % 2; // 1.5 С отрицательным дивидендом -13 % 5; // -3 -1 % 2; // -1 -4 % 2; // -0 С NaN NaN % 2; // NaN С бесконечностью Infinity % 2; // NaN Infinity % 0; // NaN Infinity % Infinity; // NaN 2 % Infinity; // 2 0 % Infinity; // 0

Целочисленное деление в JavaScript

В JavaScript есть всемогущий Math Объект, предоставляющий массу математических функций, от констант до функций. Важно отметить, что объект Math не работает с BigInt. Внутри Math Объекта находятся статические свойства и методы, обеспечивающие математические функции. Похожий объект есть и в другом языке: Math.Abs() возвращает в C# модуль числа.

Одним из статических методов является floor метод, округляющий Number Аргумент типа в меньшую сторону. Таким образом, для достижения в JS целочисленного деления мы можем выполнить типичное действие и округлить ответ (число с плавающей запятой) в меньшую сторону, используя метод, чтобы получить результат.

const y = 34; const x = 12; const quotient = Math.floor(y / x); console.log(quotient);

Деление без остатка в Python

Используйте оператор floor//, чтобы произвести в Python деление без остатка.

Floor всегда возвращает целое число и похож на использование математического деления с floor() функцией, примененной к результату.

Используется floor, чтобы вернуть целое число из целочисленного деления.Результатом использования оператора является математическое деление с floor() применением к результату функции.

Нужны разработчики в команду?

Подберём инженеров нужного стека под ваш проект — быстрый старт.

Обсудить команду

Возведение в степень (**)

Оператор возведения в степень js ( **) возвращает результат возведения первого операнда в степень второго операнда.

Оператор возведения в степень право ассоциативен: a ** b ** cравен a ** (b ** c). В Python степень (оператор возведения) определяется как имеющий более высокий приоритет, то же самое происходит и в других языках (например, PHP).

В JavaScript невозможно написать неоднозначное выражение возведения в степень. Например, -2 ** 2 в Bash это 4, а в других языках (таких как Python) — -4. Это недопустимо в JavaScript, так как операция неоднозначна. Вы должны заключить обе стороны в скобки — например, as -(2 ** 2)— чтобы сделать намерение недвусмысленным.

Обратите внимание, что некоторые языки программирования используют символ вставки ^ для перемножения степеней, но JavaScript использует этот символ для побитового логического оператора XOR .

Пример Основное возведение в степень 2 ** 3; // 8 3 ** 2; // 9 3 ** 2.5; // 15.588457268119896 10 ** -1; // 0.1

Частые вопросы (FAQ)

Что такое битовые операции?

Битовые операции — это операторы AND (&), OR (|), XOR (^), NOT (~) и сдвиги (<<, >>, >>>), которые работают напрямую с двоичным представлением числа, обрабатывая каждый бит отдельно. В отличие от обычной арифметики они выполняются за одну машинную инструкцию процессора, поэтому применяются для оптимизации в системном программировании, графике и сетевых протоколах.

Чем оператор & отличается от логического &&?

& — это побитовый оператор: он применяется одновременно ко всем битам числа и возвращает число. && — логический оператор: он работает с булевыми значениями true/false, использует короткое замыкание (не вычисляет правый операнд, если левый уже false) и возвращает булево значение. if (a & b) и if (a && b) почти всегда дают разный результат.

Для чего используют XOR на практике?

XOR применяют для простого симметричного шифрования (повторное XOR с тем же ключом восстанавливает исходные данные), обмена значений двух переменных без временной переменной и поиска единственного неповторяющегося элемента в массиве — всё это работает благодаря свойству x ^ x = 0.

Как битовые сдвиги ускоряют умножение и деление?

Сдвиг влево (<<) на один разряд эквивалентен умножению на 2, а сдвиг вправо (>>) — целочисленному делению на 2, потому что именно так устроена позиционная двоичная система счисления. Такая операция быстрее универсального умножения или деления, поэтому её используют в высоконагруженных алгоритмах и низкоуровневом коде.

Что такое битовая маска и зачем она нужна?

Битовая маска — это число, в котором отдельные биты обозначают независимые флаги (например, права доступа на чтение, запись, выполнение). Маска позволяет проверять, включать и выключать конкретный флаг операциями &, | и &~ без десятка отдельных булевых переменных, экономя память и упрощая код.

В чём разница между >> и >>> в JavaScript?

>> — арифметический сдвиг вправо, он сохраняет знак числа, заполняя освобождающиеся биты значением знакового бита. >>> — логический (беззнаковый) сдвиг, он всегда заполняет освобождающиеся биты нулями, поэтому для отрицательных чисел даёт совсем другой, обычно очень большой положительный, результат.

В каких языках программирования есть битовые операции?

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

Веб-услуги и разработка в YuSMP Group включает в себя большой спектр работ по программированию. Наши специалисты владеют актуальными технологиями и сильными навыками, чтобы создавать качественные продукты. Посмотрите проекты, которые мы создали, чтобы убедиться в высоком уровне команды.

Услуги мобильной разработки
Узнайте цену

Обсудим ваш проект?

Соберём команду под вашу задачу и оценим проект за 2 дня — бесплатно.

Обсудить проект