Что такое эти единички и нолики, из которых состоят данные? Это не абстрактные понятия, а инструменты, которыми на 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. Поэтому оператор «И» идеален для:
- Проверки того, задан ли конкретный бит.
- Удалении конкретных битов при сохранении других неизменными.
- Создания битовых масок.
Вот практический пример — проверка четности числа:
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. Реальные сценарии:
- Задание конкретных битов без влияния на другие.
- Объединение флагов в одно значение.
- Добавление свойств к набору параметров.
Вот как оператором «ИЛИ» задаются флаги:
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, только если биты отличаются. Благодаря этому уникальному свойству оператор полезен при:
- Переключении битов.
- Поиске различий между числами.
- Простом шифровании.
- Замене чисел без временной переменной.
Вот классическая замена при помощи «исключающего “ИЛИ”»:
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;
}
};
Нюансы и рекомендации
- Распространение знака при сдвигах вправо
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;
}
Закрепим пройденное
Попробуйте решить эти практические задачи:
- Подсчитайте количество заданных битов в целом числе.
- Найдите единственное число, которое не повторяется в массиве дважды.
- Расположите в обратном порядке биты в целом числе.
Вот решение первой задачи:
int countSetBits(unsigned int num) {
int count = 0;
while (num) {
count += num & 1;
num >>= 1;
}
return count;
}
Побитовые операции — это не теоретические упражнения, а практические инструменты, используемые в реальных кодовых базах: от операционных систем до игровых движков.
Начните потихоньку практиковаться на этих примерах и скоро обнаружите, что рука сама потянется к побитовым операциям, когда они станут привычным инструментом в работе.
Читайте также:
- C++: полное руководство по функциям Floor и Ceil
- C++: полное руководство по бинарной сортировке
- C++: полное руководство по std::stoi
Читайте нас в Telegram, VK и Дзен
Перевод статьи ryan: Bitwise Operations in C++: A Practical Guide





