Что такое эти единички и нолики, из которых состоят данные? Это не абстрактные понятия, а инструменты, которыми на C++ манипулируют напрямую. Благодаря побитовым операциям, работе с отдельными бита́ми, открываются возможности для всего  —  от быстрых вычислений до экономичных по расходу памяти структур данных.

Что же происходит на битовом уровне?

Прежде чем переходить к операторам, посмотрим, с чем мы на самом деле работаем. На C++, когда пишем:

int x = 5;

Число 5 сохраняется в двоичном виде как 00000000 00000000 00000000 00000101. Каждая позиция представлена степенью числа 2, справа налево: ²⁰, ²¹, ²² и так далее. Это важно, ведь при побитовых операциях работа ведется непосредственно с этими отдельными бита́ми.

Основные побитовые операторы

«И» (&): сохранение битов только там, где в обоих числах имеются единицы

int a = 12;    // 1100 в двоичном представлении
int b = 10; // 1010 в двоичном представлении
int result = a & b; // 1000, то есть 8 в десятичном виде

Каждый бит в результате равен 1, только если оба входных бита равны 1. Поэтому оператор «И» идеален для:

  1. Проверки того, задан ли конкретный бит.
  2. Удалении конкретных битов при сохранении других неизменными.
  3. Создания битовых масок.

Вот практический пример  —  проверка четности числа:

bool isEven(int num) {
return !(num & 1); // Возвращается «true», если последний бит — «0»
}

«ИЛИ» (|): задание битов там, где в одном из чисел имеются единицы

int a = 12;    // 1100 в двоичном представлении
int b = 10; // 1010 в двоичном представлении
int result = a | b; // 1110, то есть 14 в десятичном виде

Оператором «ИЛИ» каждому биту задается значение 1, если один из входных битов равен 1. Реальные сценарии:

  1. Задание конкретных битов без влияния на другие.
  2. Объединение флагов в одно значение.
  3. Добавление свойств к набору параметров.

Вот как оператором «ИЛИ» задаются флаги:

enum FilePermissions {
READ = 1, // 001
WRITE = 2, // 010
EXECUTE = 4 // 100
};

int permissions = READ | WRITE; // Создается «011»

«Исключающее “ИЛИ”» (^): задание битов там, где входные отличаются

int a = 12;    // 1100 в двоичном представлении
int b = 10; // 1010 в двоичном представлении
int result = a ^ b; // 0110, то есть 6 в десятичном виде

«Исключающим “ИЛИ”» выдается 1, только если биты отличаются. Благодаря этому уникальному свойству оператор полезен при:

  1. Переключении битов.
  2. Поиске различий между числами.
  3. Простом шифровании.
  4. Замене чисел без временной переменной.

Вот классическая замена при помощи «исключающего “ИЛИ”»:

void xorSwap(int& a, int& b) {
if (&a != &b) { // Проверяется, что это не одна и та же переменная
a ^= b;
b ^= a;
a ^= b;
}
}

Операторы сдвига << и >>: перемещение битов влево или вправо

int x = 8;     // 1000 в двоичном представлении
int left = x << 1; // 10000, то есть 16 в десятичном виде
int right = x >> 1; // 0100, то есть 4 в десятичном виде

Сдвиги влево умножаются на 2, сдвиги вправо делятся на 2. Но они быстрее, чем само умножение или деление, ведь работа ведется с бита́ми напрямую.

Реальные применения

1. Быстрые умножение и деление целых чисел

int fastMultiplyBy2(int num) {
return num << 1;
}

int fastDivideBy2(int num) {
return num >> 1;
}

// Еще более быстрое умножение числа степени 2
int multiplyBy8(int num) {
return num << 3; // Умножается на 2³
}

2. Манипулирование цветом RGB

class Color {
uint32_t value; // RGB сохраняется в одном целом числе
public:
Color(uint8_t r, uint8_t g, uint8_t b) {
value = (r << 16) | (g << 8) | b;
}

uint8_t getRed() const {
return (value >> 16) & 0xFF;
}

uint8_t getGreen() const {
return (value >> 8) & 0xFF;
}

uint8_t getBlue() const {
return value & 0xFF;
}
};

3. Булев массив с экономичным расходом памяти

class BitSet {
vector<uint32_t> bits;
size_t size;

public:
BitSet(size_t n) : size(n) {
bits.resize((n + 31) / 32, 0);
}

void set(size_t pos) {
if (pos < size) {
bits[pos / 32] |= (1u << (pos % 32));
}
}

void clear(size_t pos) {
if (pos < size) {
bits[pos / 32] &= ~(1u << (pos % 32));
}
}

bool test(size_t pos) const {
if (pos >= size) return false;
return bits[pos / 32] & (1u << (pos % 32));
}
};

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

4. Заголовки сетевых протоколов

struct IPv4Header {
uint8_t version_ihl; // Версия протокола и длина заголовка IP объединены

void setVersion(uint8_t ver) {
version_ihl = (version_ihl & 0x0F) | (ver << 4);
}

void setIHL(uint8_t ihl) {
version_ihl = (version_ihl & 0xF0) | (ihl & 0x0F);
}

uint8_t getVersion() const {
return (version_ihl >> 4) & 0x0F;
}

uint8_t getIHL() const {
return version_ihl & 0x0F;
}
};

Нюансы и рекомендации

  1. Распространение знака при сдвигах вправо
int x = -8;
int result = x >> 1; // «–4» получается не на всех платформах

// Решение: для битовых манипуляций использовать «unsigned»
unsigned int y = 8;
unsigned int safeResult = y >> 1;

2. Неопределенное поведение при больших сдвигах

int x = 1 << 31;  // Неопределенное поведение на 32-битном «int»

// Решение: использовать «unsigned» и проверить величину сдвига
unsigned int safe = 1u << 31; // Определенное поведение

3. Порядок операций

int x = 5;
// Это неправильно:
if (x & 1 == 0) { } // Приоритетность «==» выше, чем у «&»

// Корректная версия:
if ((x & 1) == 0) { }

Отладка побитовых операций

При работе с бита́ми вывод двоичных представлений бесценен:

void printBinary(unsigned int num) {
for(int i = 31; i >= 0; i--) {
cout << ((num >> i) & 1);
if (i % 8 == 0) cout << ' ';
}
cout << endl;
}

Закрепим пройденное

Попробуйте решить эти практические задачи:

  1. Подсчитайте количество заданных битов в целом числе.
  2. Найдите единственное число, которое не повторяется в массиве дважды.
  3. Расположите в обратном порядке биты в целом числе.

Вот решение первой задачи:

int countSetBits(unsigned int num) {
int count = 0;
while (num) {
count += num & 1;
num >>= 1;
}
return count;
}

Побитовые операции  —  это не теоретические упражнения, а практические инструменты, используемые в реальных кодовых базах: от операционных систем до игровых движков.

Начните потихоньку практиковаться на этих примерах и скоро обнаружите, что рука сама потянется к побитовым операциям, когда они станут привычным инструментом в работе.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи ryan: Bitwise Operations in C++: A Practical Guide

Предыдущая статьяТоп-12 языков программирования для корпоративного ПО в 2026 году