Что такое битовые операции в C/C++?

Ответ

Битовые операции в 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). Там всё предсказуемо: сдвиг вправо — всегда дополнение нулями. И волнение ебать отпадёт.