Поразрядные операции: старший и младший разряд, маски и сдвиги

Поразрядные операции: старший и младший разряд, маски и сдвиги Полезное

Младший разряд (младший бит, LSB) — крайний правый бит двоичной записи числа, его вес 2^0 = 1. Старший разряд (старший бит, MSB) — крайний левый бит числа фиксированной разрядности N, его вес 2^(N-1); в знаковых целых (дополнительный код) это знаковый бит: 0 — число неотрицательное, 1 — отрицательное.

Поразрядные (битовые) операции работают с каждым разрядом числа по отдельности: И &, ИЛИ |, исключающее ИЛИ ^, инверсия ~ и сдвиги <<, >>. Ниже — как найти старший и младший бит, как маской установить, сбросить и проверить бит, и чем опасны сдвиги отрицательных чисел. Примеры проверены на C++20 (Apple clang 21), Python 3.14, Java 25 и Node.js 24.

Старший и младший разряд на примере числа 149

Число 149 в 8 битах записывается как 10010101. Разряды нумеруют справа налево с нуля:

Номер бита 7 6 5 4 3 2 1 0
Вес 128 64 32 16 8 4 2 1
Значение 1 0 0 1 0 1 0 1

128 + 16 + 4 + 1 = 149. Бит 0 — младший, бит 7 — старший. Четыре термина, которые часто смешивают:

  • разрядность — сколько бит отведено под число (8, 16, 32, 64); без нее «старший бит» не определен;
  • MSB — левый бит в пределах разрядности, даже если он равен 0 (у числа 5 в 8 битах 00000101 MSB = 0);
  • старшая единица — самая левая единица числа (у 5 это бит 2), ее номер ищут через bit_length() в Python или std::bit_width в C++;
  • знаковый бит — MSB у знакового типа в дополнительном коде.

Порядок байтов в памяти (little-endian, big-endian) — отдельная тема: он касается расположения байтов многобайтового числа, а не нумерации битов в значении. Подробно он разобран в статье о порядке байтов и битов.

Шесть поразрядных операций

Возьмем a = 12 (00001100) и b = 10 (00001010), тип — беззнаковый 8-битный:

Операция Правило для каждого разряда Результат Десятичное
a & b 1, если оба бита 1 00001000 8
a | b 1, если хотя бы один бит 1 00001110 14
a ^ b 1, если биты разные 00000110 6
~a каждый бит наоборот 11110011 243
a << 2 сдвиг влево, справа входят нули 00110000 48
a >> 2 сдвиг вправо, младшие биты выпадают 00000011 3

Таблица показывает биты в пределах 8 разрядов. В C и C++ операнды типа uint8_t перед операцией расширяются до int, поэтому ~a там дает -13 (все 32 бита инвертированы), а 243 получится только после приведения обратно: static_cast<std::uint8_t>(~a). Так же сделано в примере с флагами ниже.

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

Сдвиг влево на k в пределах разрядности равен умножению на 2^k, сдвиг вправо беззнакового числа — целочисленному делению на 2^k. Биты, вышедшие за границу типа, теряются, поэтому «сдвиг = умножение» верно только без переполнения.

Маски: установить, сбросить, переключить и проверить бит

Маска — число, в котором единицы стоят в нужных позициях. Маску для бита n получают сдвигом 1u << n. Так хранят набор флагов в одном целом: права доступа, настройки, состояние устройства.

#include <bitset>
#include <cstdint>
#include <iostream>

// Каждый флаг - отдельный бит (маска с одной единицей)
constexpr std::uint8_t READ  = 1u << 0;  // 0000'0001
constexpr std::uint8_t WRITE = 1u << 1;  // 0000'0010
constexpr std::uint8_t EXEC  = 1u << 2;  // 0000'0100

void show(const char* what, std::uint8_t p) {
    std::cout << what << std::bitset<8>(p) << '\n';
}

int main() {
    std::uint8_t perms = 0;
    perms |= READ | WRITE;              // установить биты
    show("set R|W:   ", perms);
    perms &= static_cast<std::uint8_t>(~WRITE);  // сбросить бит
    show("clear W:   ", perms);
    perms ^= EXEC;                      // переключить бит
    show("toggle X:  ", perms);
    bool canRead = (perms & READ) != 0; // проверить бит
    std::cout << "can read:  " << std::boolalpha << canRead << '\n';

    std::uint8_t x = 149;               // 1001'0101
    show("149:       ", x);
    std::cout << "LSB (bit 0): " << (x & 1) << '\n';
    std::cout << "MSB (bit 7): " << ((x >> 7) & 1) << '\n';
}

Сборка g++ -std=c++20 -Wall -Wextra flags.cpp, вывод:

set R|W:   00000011
clear W:   00000001
toggle X:  00000101
can read:  true
149:       10010101
LSB (bit 0): 1
MSB (bit 7): 1

Четыре приема в одной таблице:

Задача Выражение Почему работает
Установить бит n x |= 1u << n ИЛИ с 1 дает 1, с 0 не меняет бит
Сбросить бит n x &= ~(1u << n) И с 0 дает 0, остальные биты под единицами маски
Переключить бит n x ^= 1u << n XOR с 1 инвертирует, с 0 не меняет
Проверить бит n (x >> n) & 1 или (x & (1u << n)) != 0 в результате остается только нужный бит

Младший бит проверяют через x & 1 (это же проверка на нечетность), старший бит N-битного числа — через (x >> (N - 1)) & 1.

Сдвиги и знак: арифметический и логический

У отрицательных чисел сдвиг вправо бывает двух видов. Арифметический копирует знаковый бит в освободившиеся старшие разряды и сохраняет знак: -16 >> 2 = -4. Логический всегда заполняет старшие разряды нулями, и отрицательное число становится большим положительным.

Язык >> для знаковых Логический сдвиг -16 >> 2 / логический
C++20 арифметический (гарантирован с C++20) привести к беззнаковому: uint32_t(x) >> 2 -4 / 1073741820
Java 25 арифметический >>> -4 / 1073741820
JavaScript (Node.js 24) арифметический, над 32-битным int >>> (результат беззнаковый 32-битный) -4 / 1073741820
Python 3.14 арифметический (деление с округлением вниз) нет оператора, нужна маска: (x & 0xFFFFFFFF) >> 2 -4 / 1073741820

До C++20 сдвиг вправо отрицательного числа был implementation-defined, а сдвиг влево отрицательного — неопределенным поведением. В старом коде надежнее сдвигать беззнаковые типы.

В Java и JavaScript количество сдвига для int берется по модулю 32, поэтому 1 << 32 дает 1, а не 0 (в Java для long берется по модулю 64, и 1L << 32 = 4294967296). В JavaScript любая битовая операция сначала приводит число к 32-битному целому: 1 << 31 = -2147483648, 2 ** 32 | 0 = 0.

Python: неограниченные целые и инверсия

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

x = 149
print(bin(x), x.bit_length(), x.bit_count())
print(x & 1, (x >> 7) & 1)
print(~5, -16 >> 2)
print(bin(~5 & 0xFF))
u = -16 & 0xFFFFFFFF
print(u >> 2)
print(1 << 100)

Вывод на Python 3.14:

0b10010101 8 4
1 1
-6 -4
0b11111010
1073741820
1267650600228229401496703205376

bit_length() вернул 8 — номер старшей единицы плюс один, bit_count() (есть с Python 3.10) посчитал четыре единицы. ~5 равно -6, потому что для целых Python ~x == -x - 1; чтобы увидеть инверсию в 8 битах, результат обрезают маской & 0xFF. Сдвиг 1 << 100 не переполняется — число просто растет.

Типичные ошибки

Приоритет & ниже, чем ==. В C, C++, Java и JavaScript выражение x & 1 == 0 читается как x & (1 == 0).

#include <iostream>
int main() {
    int x = 6;
    if (x & 1 == 0) std::cout << "even\n";
    else std::cout << "odd?\n";
}

Программа печатает odd?, хотя 6 четное. Clang 21 с -Wall предупреждает: warning: & has lower precedence than ==; == will be evaluated first [-Wparentheses]. Исправление — скобки: if ((x & 1) == 0). В Java такой код не скомпилируется из-за несовместимых типов, в Python приоритет & выше, чем у сравнений, и ошибки нет.

Маска шире типа. std::uint64_t mask = 1 << 40; сдвигает int, а не 64-битное число. Сдвиг на величину не меньше разрядности — неопределенное поведение: clang 21 выдал warning: shift count >= width of type, и программа печатала при каждом запуске разное число, а GCC 14 с тем же кодом напечатал 0. Разный результат на разных компиляторах — типичный признак неопределенного поведения. Исправление — сдвигать литерал нужной ширины: 1ULL << 40 = 1099511627776.

Сдвиг знакового числа как деление. В C++ и Java -7 >> 1 дает -4 (округление вниз), а -7 / 2 дает -3 (округление к нулю). Для отрицательных чисел это разные операции.

Короткие приемы с битами

Задача Выражение (C++) Для x = 40 (101000)
Четность x & 1 0 — четное
Степень двойки (x > 0) (x & (x - 1)) == 0 false
Младшая единица x & (~x + 1) 8
Число единиц std::popcount(x) 2
Номер старшей единицы + 1 std::bit_width(x) 6

std::popcount и std::bit_width находятся в заголовке <bit> (C++20) и принимают только беззнаковые типы. Трюк с обменом через три XOR без временной переменной работает, но читается хуже std::swap и ломается, если обе ссылки указывают на одну переменную.

Выводы

  • Младший разряд — правый бит с весом 1, старший — левый бит в пределах разрядности N с весом 2^(N-1); у знаковых типов он же знаковый.
  • MSB и старшая единица числа — разные вещи: первый зависит от типа, вторая от значения.
  • Маски решают четыре задачи: |= устанавливает, &= ~ сбрасывает, ^= переключает, & проверяет бит.
  • Сдвиг вправо отрицательных чисел арифметический в C++20, Java, JS и Python; логический сдвиг — >>> в Java/JS или беззнаковый тип и маска.
  • Частые ошибки — приоритет & ниже == и сдвиг int на ширину 32 и более.

Где применяется / связь с практикой

Освойте тему на практике

Битовые операции нужны в драйверах и встраиваемых системах (регистры устройств), сетевом коде (маски подсетей, флаги заголовков), графике (упаковка цвета в 32 бита), хеш-функциях и компактных структурах вроде битовых множеств. В C++ с ними сталкиваются почти сразу: флаги потоков, std::bitset, работа с бинарными форматами. Систематически разобрать типы, приведения и неопределенное поведение можно на курсе «C++-разработчик. Базовый уровень», а посмотреть формат занятий — на открытых уроках Otus.

FAQ

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

Почему ~0 в Python равно -1, а не большому числу?
Целые в Python не имеют фиксированной ширины и ведут себя как бесконечный дополнительный код, где ~x == -x - 1. Беззнаковый вариант получают маской: ~0 & 0xFF = 255.

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

OTUS Журнал