Ответ
Битовые операции в C/C++ — это низкоуровневые операции, которые работают непосредственно с отдельными битами целочисленных типов данных (int, char, uint32_t и т.д.). Они выполняются процессором очень быстро и часто используются для оптимизации, управления флагами, работы с аппаратными регистрами или реализации компактных структур данных.
Основные операторы:
| Оператор | Название | Описание | Пример (a=0b1100, b=0b1010) |
|---|---|---|---|
& |
Побитовое И (AND) | Бит результата = 1, если оба соответствующих бита операндов = 1. | a & b = 0b1000 |
| |
Побитовое ИЛИ (OR) | Бит результата = 1, если хотя бы один из битов = 1. | a | b = 0b1110 |
^ |
Исключающее ИЛИ (XOR) | Бит результата = 1, если биты операндов различны. | a ^ b = 0b0110 |
~ |
Побитовое НЕ (NOT) | Инверсия всех битов операнда. | ~a = 0b...11110011 |
<< |
Сдвиг влево (Left Shift) | Сдвигает биты влево, заполняя младшие биты нулями. Эквивалентно умножению на 2^n. | a << 2 = 0b110000 (48) |
>> |
Сдвиг вправо (Right Shift) | Сдвигает биты вправо. Для беззнаковых типов — заполняет старшие биты нулями. Для знаковых — зависит от компилятора (обычно знаковый бит). Эквивалентно целочисленному делению на 2^n. | (unsigned)a >> 2 = 0b0011 (3) |
Практическое применение: работа с набором флагов (битовой маской).
#include <cstdint>
#include <iostream>
// Определяем флаги как степени двойки (один установленный бит)
enum class UserPermissions : uint8_t {
NONE = 0b00000000, // 0
READ = 0b00000001, // 1 << 0
WRITE = 0b00000010, // 1 << 1
EXECUTE = 0b00000100, // 1 << 2
ADMIN = 0b10000000 // 1 << 7
};
int main() {
uint8_t myPermissions = 0;
// Установка флагов (добавление прав) с помощью OR
myPermissions |= static_cast<uint8_t>(UserPermissions::READ);
myPermissions |= static_cast<uint8_t>(UserPermissions::WRITE);
// myPermissions теперь = 0b00000011 (READ + WRITE)
// Проверка наличия флага с помощью AND
bool canWrite = (myPermissions & static_cast<uint8_t>(UserPermissions::WRITE)) != 0; // true
bool isAdmin = (myPermissions & static_cast<uint8_t>(UserPermissions::ADMIN)) != 0; // false
// Снятие флага (сброс бита)
myPermissions &= ~static_cast<uint8_t>(UserPermissions::WRITE); // Убираем право WRITE
// myPermissions теперь = 0b00000001 (только READ)
// Переключение флага (XOR)
myPermissions ^= static_cast<uint8_t>(UserPermissions::EXECUTE); // Добавляем EXECUTE
myPermissions ^= static_cast<uint8_t>(UserPermissions::EXECUTE); // Убираем EXECUTE
std::cout << "Permissions byte: " << std::hex << (int)myPermissions << std::endl;
return 0;
}
Важно: При сдвигах вправо знаковых чисел (int, short) результат зависит от реализации (архитектурно-зависимое поведение). Для переносимости лучше работать с беззнаковыми типами (unsigned int, uint32_t).
Ответ 18+ 🔞
А, битовые операции! Ну, это как раз тот случай, когда можно почувствовать себя настоящим шаманом, который шепчет на языке процессора. Смотри, тут всё просто, если не загоняться.
Представь, что у тебя есть число. Не просто число, а набор крошечных переключателей — битов. Ноль — выключено, единица — включено. И есть куча операций, чтобы этими переключателями дёргать, не трогая остальные. Это овердохуища быстрее, чем если бы ты с обычными переменными возился.
Вот основные заклинания, прости, операторы:
| Оператор | Название | Что делает | Пример ( a=0b1100, b=0b1010 ) |
|---|---|---|---|
& |
Побитовое И (AND) | Даёт единицу только если у обоих чисел в этом разряде единица. Как строгий отец: «Разрешаю только если мама и я согласны». | a & b = 0b1000 |
| |
Побитовое ИЛИ (OR) | Даёт единицу, если хотя бы у одного есть единица. Либеральная мама: «Ладно, пусть будет, если хоть кто-то разрешил». | a | b = 0b1110 |
^ |
Исключающее ИЛИ (XOR) | Даёт единицу, если биты разные. Логика подростка: «Я сделаю наоборот, лишь бы не как у родителей». | a ^ b = 0b0110 |
~ |
Побитовое НЕ (NOT) | Просто инвертирует все биты. Ноль становится единицей, единица — нулём. Полное отрицание, как у Гамлета, только техническое. | ~a = 0b...11110011 |
<< |
Сдвиг влево | Сдвигает все биты влево, справа дописывает нули. Это как умножение на 2^n. Сдвинул на 2 — умножил на 4. | a << 2 = 0b110000 (48) |
>> |
Сдвиг вправо | Сдвигает биты вправо. Тут, ёпта, внимание: для беззнаковых чисел слева нули подтягиваются. Для обычных int — хрен его знает, зависит от компилятора, может и знаковый бит копировать. Лучше с беззнаковыми работать, чтоб голова не болела. Эквивалентно делению на 2^n. | (unsigned)a >> 2 = 0b0011 (3) |
Где это, блядь, применить? Да везде! Самый жирный кейс — флаги и права доступа. Вместо того чтобы городить кучу bool переменных, ты упаковываешь всё в одно число. Экономия памяти — просто пиздец.
#include <cstdint>
#include <iostream>
// Объявляем флаги как степени двойки. Каждый флаг — один отдельный бит.
enum class UserPermissions : uint8_t {
NONE = 0b00000000, // 0
READ = 0b00000001, // 1 << 0
WRITE = 0b00000010, // 1 << 1
EXECUTE = 0b00000100, // 1 << 2
ADMIN = 0b10000000 // 1 << 7
};
int main() {
uint8_t myPermissions = 0; // Изначально прав — ноль, полный ноль.
// ДОБАВИТЬ право (установить бит в 1). Используем OR (|).
myPermissions |= static_cast<uint8_t>(UserPermissions::READ);
myPermissions |= static_cast<uint8_t>(UserPermissions::WRITE);
// Теперь в myPermissions: 0b00000011 (есть READ и WRITE)
// ПРОВЕРИТЬ право. Используем AND (&). Если результат не ноль — право есть.
bool canWrite = (myPermissions & static_cast<uint8_t>(UserPermissions::WRITE)) != 0; // true
bool isAdmin = (myPermissions & static_cast<uint8_t>(UserPermissions::ADMIN)) != 0; // false, конечно
// УБРАТЬ право (сбросить бит в 0). Хитрый трюк: инвертируем маску флага (~) и делаем AND.
myPermissions &= ~static_cast<uint8_t>(UserPermissions::WRITE); // Убираем WRITE
// Теперь: 0b00000001 (остался только READ)
// ПЕРЕКЛЮЧИТЬ право (если было 1 — станет 0, и наоборот). Магия XOR (^).
myPermissions ^= static_cast<uint8_t>(UserPermissions::EXECUTE); // Добавили EXECUTE
myPermissions ^= static_cast<uint8_t>(UserPermissions::EXECUTE); // И тут же убрали
std::cout << "Permissions byte: " << std::hex << (int)myPermissions << std::endl;
return 0;
}
Главный подвох, на котором все обжигаются: сдвиг вправо (>>) для обычных, знаковых int. Поведение, блядь, зависит от компилятора и железа! Может заполняться нулями, а может «знаковым битом». Поэтому если хочешь спать спокойно и не гадать, «а что же у меня там в старших битах», работай с беззнаковыми типами (unsigned int, uint32_t). Там всё предсказуемо: сдвиг вправо — всегда дополнение нулями. И волнение ебать отпадёт.