Младший разряд (младший бит, LSB) — крайний правый бит двоичной записи числа, его вес 2^0 = 1. Старший разряд (старший бит, MSB) — крайний левый бит числа фиксированной разрядности N, его вес 2^(N-1); в знаковых целых (дополнительный код) это знаковый бит: 0 — число неотрицательное, 1 — отрицательное.
Содержание
- Старший и младший разряд на примере числа 149
- Шесть поразрядных операций
- Маски: установить, сбросить, переключить и проверить бит
- Сдвиги и знак: арифметический и логический
- Python: неограниченные целые и инверсия
- Типичные ошибки
- Короткие приемы с битами
- Выводы
- Где применяется / связь с практикой
- FAQ
Поразрядные (битовые) операции работают с каждым разрядом числа по отдельности: И &, ИЛИ |, исключающее ИЛИ ^, инверсия ~ и сдвиги <<, >>. Ниже — как найти старший и младший бит, как маской установить, сбросить и проверить бит, и чем опасны сдвиги отрицательных чисел. Примеры проверены на 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 битах
00000101MSB = 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?
Современные компиляторы сами заменяют умножение на степень двойки сдвигом, поэтому выигрыша обычно нет. Пишите то, что точнее выражает смысл: арифметику — умножением, работу с битами — сдвигом.



