Главная / Курсы / C++ по спирали / Глава 2. Базовый синтаксис / Магия побитовых операторов
# Глава 2.3. Магия побитовых операторов Побитовые операторы (bitwise operators) нужны для манипуляции _над отдельными битами_ значений. Они применимы только к целым числам. Начинающие разработчики недооценивают побитовые операторы и побаиваются их. И зря, потому что они повсеместно применяются в реальной практике. Их можно отыскать в любой прикладной области, а не только в сжатии и (де)кодировании данных, работе с аудио/видео, криптографии или разработке микроконтроллеров. Вот популярные, но далеко не единственные сценарии применения битовых операций: - Быстрая арифметика. В ближайшей [практической работе](/courses/cpp/practice/cpp_div_without_div/) вы сами реализуете деление через побитовый сдвиг. - Упаковка данных. В _одной_ переменной можно хранить _несколько_ значений, и в этой главе вы узнаете, как. Кстати, а зачем в одну переменную упаковывать несколько значений? Дело в том, что минимальная адресуемая ячейка памяти — 1 байт. Размер любой переменной кратен байту: нельзя создать переменную размером в 2 бита или 17 бит. Однако нередки ситуации, когда диапазон значений переменной ограничен и известен заранее. Например, целое число, принимающее значения от 0 до 3-х. Для его хранения хватит и 2-х бит, а в один байт поместится аж 4 таких значения! Если состоящих из маленьких значений данных становится много, возникает необходимость экономить память и хранить их _компактно._ Как этого добиться? Для начала вспомним, каким образом числа представлены в памяти. ## Бинарное представление чисел Каждое число в памяти представлено в виде последовательности бит: - 5 в бинарном виде — это `0b101`, - 10 — это `0b1010`, - 7 — это `0b111`, и так далее. Префикс `0b` в C++ используется для записи значений в двоичной системе счисления. Если вы плохо с ней знакомы, самое время [восполнить знания.]( https://ru.wikipedia.org/wiki/%D0%94%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D0%B0%D1%8F_%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B0_%D1%81%D1%87%D0%B8%D1%81%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F) Для удобства длинные числа можно разделять апострофом: `0b111'010`. Сколько именно бит в памяти занимает число? Это зависит от типа числа и целевой платформы, под которую компилируется код. Например, под значения типа `std::size_t` _чаще всего_ выделяется 4 байта на 32-битных системах и 8 байт на 64-битных. Так выглядит в памяти число 3 беззнакового типа `std::size_t`, занимающее 4 байта: ``` ┌───────────┬──────────┬──────────┬──────────┬──────────┐ │ Значения │ │ │ │ │ │ байтов │ 00000000 │ 00000000 │ 00000000 │ 00000011 │ ├───────────┼──────────┼──────────┼──────────┼──────────┤ │ Индексы │ │ │ │ │ │ байтов │ Байт 3 │ Байт 2 │ Байт 1 │ Байт 0 │ └───────────┴──────────┴──────────┴──────────┴──────────┘ ``` Обратите внимание, что нумерация байтов идёт справа налево, а не наоборот. Это не ошибка, а договорённость: индекс 0 относится к младшему байту, а максимальный индекс — к старшему байту. Так заведено, чтобы при увеличении разрядности числа не пришлось менять индексы старых байтов. Например, если число будет занимать 8 байтов вместо 4-х, то добавятся старшие байты с индексами 4, 5, 6 и 7. А индексы младших байтов при этом не изменятся. ## Побитовые операторы Побитовых операторов всего шесть: - `~a` — побитовое отрицание (инверсия), - `a & b` — побитовое «И», - `a | b` — побитовое «ИЛИ», - `a ^ b` — побитовый XOR (взаимоисключающее «ИЛИ»), - `a << n` — побитовый сдвиг влево: смещение битов числа `a` на `n` позиций влево, - `a >> n` — побитовый сдвиг вправо: смещение битов числа `a` на `n` позиций вправо. ## Побитовое отрицание: оператор ~ Оператор `~` — это побитовое «НЕ»: `~a` инвертирует биты числа `a`. Нули становятся единицами, а единицы — нулями. ``` ┌───┬────┐ │ a │ ~a │ ├───┼────┤ │ 0 │ 1 │ ├───┼────┤ │ 1 │ 0 │ └───┴────┘ ``` Допустим, у нас есть беззнаковое число `x`: ```cpp std::size_t x = 5; ``` При условии что `std::size_t` занимает 4 байта, в памяти `x` будет представлен следующим образом: ``` ┌───────────┬──────────┬──────────┬──────────┬──────────┐ │ Значения │ │ │ │ │ │ байтов │ 00000000 │ 00000000 │ 00000000 │ 00000101 │ ├───────────┼──────────┼──────────┼──────────┼──────────┤ │ Нумерация │ │ │ │ │ │ байтов │ Байт 3 │ Байт 2 │ Байт 1 │ Байт 0 │ └───────────┴──────────┴──────────┴──────────┴──────────┘ ``` Применим побитовое отрицание: ```cpp std::size_t y = ~x; ``` В переменную `y` сохранится последовательность бит: ``` ┌───────────┬──────────┬──────────┬──────────┬──────────┐ │ Значения │ │ │ │ │ │ байтов │ 11111111 │ 11111111 │ 11111111 │ 11111010 │ ├───────────┼──────────┼──────────┼──────────┼──────────┤ │ Нумерация │ │ │ │ │ │ байтов │ Байт 3 │ Байт 2 │ Байт 1 │ Байт 0 │ └───────────┴──────────┴──────────┴──────────┴──────────┘ ``` Как видите, это очень большое число. В десятичной системе счисления оно равно `4 145 343 750`. Если бы `std::size_t` занимал 8 байт, а не 4, то число было бы ещё больше. Перед вами значение типа `std::size_t` размером в 4 байта. Его бинарное представление: `0b11111111'11111111'11111111'11111100`. {.task_text} Чему будет равно это число после применения к нему оператора `~`? Ответ запишите в десятичной системе счисления. {.task_text} ```consoleoutput {.task_source #cpp_chapter_0023_task_0010} ``` Оператор `~` инвертирует биты числа и в итоге получится бинарное значение `11`. {.task_hint} ```cpp {.task_answer} 3 ``` ## Побитовое «И»: оператор & Побитовый оператор `&` похож на логический «И» `&&`, но применяется к числу побитово. Операция `a & b` устанавливает бит результата в 1, если оба соответствующих бита чисел `a` и `b` равны 1: ``` ┌───┬───┬───────┐ │ a │ b │ a & b │ ├───┼───┼───────┤ │ 0 │ 0 │ 0 │ ├───┼───┼───────┤ │ 0 │ 1 │ 0 │ ├───┼───┼───────┤ │ 1 │ 0 │ 0 │ ├───┼───┼───────┤ │ 1 │ 1 │ 1 │ └───┴───┴───────┘ ``` Пример: ```cpp int x = 5 & 6; // 4 // 0b101 - 5 // & 0b110 - 6 // ----- // 0b100 - 4 ``` Чему равен результат выполнения операции `0b11 & 0b1101`? Ответ введите в бинарном формате без префикса `0b`. {.task_text} ```consoleoutput {.task_source #cpp_chapter_0023_task_0020} ``` Добавим в первый операнд два нуля слева: `0b0011 & 0b1101`. {.task_hint} ```cpp {.task_answer} 1 ``` Кстати, через побитовое «И» можно проверить, чётное число или нечётное: ```cpp {.example_for_playground .example_for_playground_001} if ((n & 1) == 1) { // n - нечётное } ``` Разберём, как это работает. У литерала `1` все биты, кроме младшего (нулевого), установлены в `0`. Поэтому у результата операции `n & 1` все биты, кроме нулевого, гарантированно равны 0. А нулевой бит будет равен 1 только в случае, если младший бит числа `n` тоже равен 1. А младший бит числа равен 1 только у нечётных чисел. Это легко заметить: `0b1 == 1`, `0b10 == 2`, `0b11 == 3`, `0b100 == 4` и так далее. ## Побитовое «ИЛИ»: оператор | Побитовый оператор `|` похож на логический «ИЛИ» `||`, но применяется к числу побитово. Операция `a | b` устанавливает бит результата в 1, если любой из соответствующих битов чисел `a` и `b` равен 1: ``` ┌───┬───┬───────┐ │ a │ b │ a | b │ ├───┼───┼───────┤ │ 0 │ 0 │ 0 │ ├───┼───┼───────┤ │ 0 │ 1 │ 1 │ ├───┼───┼───────┤ │ 1 │ 0 │ 1 │ ├───┼───┼───────┤ │ 1 │ 1 │ 1 │ └───┴───┴───────┘ ``` Пример: ```cpp int x = 8 | 6; // 14 // 0b1000 - 8 // | 0b0110 - 6 // ----- // 0b1110 - 14 ``` Чему равен результат выполнения операции `0b1 | 0b10`? Ответ введите в десятичной системе счисления. {.task_text} ```consoleoutput {.task_source #cpp_chapter_0023_task_0030} ``` Добавим в первый операнд 0 слева: `0b01 & 0b10`. Получим результат `0b11`. {.task_hint} ```cpp {.task_answer} 3 ``` ## Побитовый XOR: оператор ^ {#block-xor} У оператора `^` (XOR) нет аналога среди логических операторов. Он устанавливает бит в 1, если соответствующие биты чисел `a` и `b` не равны. ``` ┌───┬───┬───────┐ │ a │ b │ a ^ b │ ├───┼───┼───────┤ │ 0 │ 0 │ 0 │ ├───┼───┼───────┤ │ 0 │ 1 │ 1 │ ├───┼───┼───────┤ │ 1 │ 0 │ 1 │ ├───┼───┼───────┤ │ 1 │ 1 │ 0 │ └───┴───┴───────┘ ``` Пример: ```cpp int x = 3 ^ 5; // 6 // 0b011 - 3 // ^ 0b101 - 5 // ----- // 0b110 - 6 ``` Чему равен результат выполнения операции `0b1011 ^ 0b1011`? Ответ введите в десятичной системе счисления. {.task_text} ```consoleoutput {.task_source #cpp_chapter_0023_task_0040} ``` XOR возвращает 0, если соответствующие биты числа равны. {.task_hint} ```cpp {.task_answer} 0 ``` Кстати, через XOR можно проверять числа на равенство: ```cpp {.example_for_playground .example_for_playground_002} if ((a ^ b) == 0) { // a и b равны } ``` ## Побитовый сдвиг влево: оператор << {#block-bitwise} Оператор `<<` — это **побитовый сдвиг влево.** Запись вида `x << n` означает, что каждый бит числа `x` сдвигается влево на `n` бит. Освобождённые биты заполняются нулями. Например, если число 3 сдвинуть на 2 бита влево, мы получим 12: ```cpp 3 << 2 // 12 ``` Чтобы понять, откуда берётся 12, взглянем на беззнаковое число 3 в двоичном виде: ``` 0b00000000'00000000'00000000'00000011 ``` ``` ┌───────────┬──────────┬──────────┬──────────┬──────────┐ │ Значения │ │ │ │ │ │ байтов │ 00000000 │ 00000000 │ 00000000 │ 00000011 │ ├───────────┼──────────┼──────────┼──────────┼──────────┤ │ Нумерация │ │ │ │ │ │ байтов │ Байт 3 │ Байт 2 │ Байт 1 │ Байт 0 │ └───────────┴──────────┴──────────┴──────────┴──────────┘ ``` Сдвиг `0b11` на 2 бита влево приводит к тому, что биты числа смещаются в сторону старших разрядов, а на их место устанавливаются нули: `0b1100`. ``` ┌───────────┬──────────┬──────────┬──────────┬──────────┐ │ Значения │ │ │ │ │ │ байтов │ 00000000 │ 00000000 │ 00000000 │ 00001100 │ ├───────────┼──────────┼──────────┼──────────┼──────────┤ │ Нумерация │ │ │ │ │ │ байтов │ Байт 3 │ Байт 2 │ Байт 1 │ Байт 0 │ └───────────┴──────────┴──────────┴──────────┴──────────┘ ``` Это и есть 12 в десятичной системе счисления. Сдвиг числа на один бит влево аналогичен умножению на 2: ```cpp x << 1 == x * 2 ``` Обобщим. Сдвиг числа на `n` бит влево аналогичен умножению на 2 `n` раз, то есть умножению на 2 в степени `n`: `2 ^ n`. Примеры: ```cpp 4 << 3 // 32 1 << 4 // 16 ``` Какое значение сохранится в переменную `x`? {.task_text} ```cpp int x = 1 << 3; ``` ```consoleoutput {.task_source #cpp_chapter_0023_task_0050} ``` Это возведение 2 в степень 3. {.task_hint} ```cpp {.task_answer} 8 ``` ## Побитовый сдвиг вправо: оператор >> Оператор `>>` — это **побитовый сдвиг вправо.** Запись `x >> n` означает, что биты числа `x` сдвигаются на `n` бит вправо. Для беззнаковых целых, таких как `std::size_t`, освобождающиеся биты заполняются нулями. Для знаковых целых, таких как `int`, поведение определяется реализацией в компиляторе (implementation-defined). Как можно догадаться, операция сдвига вправо обратна сдвигу влево. И она аналогична целочисленному делению на `2 ^ n`: ```cpp 9 >> 1 // 4 21 >> 2 // 5 ``` Приоритет операторов побитового сдвига ниже, чем у сложения `+` и вычитания `-`. Но выше, чем у операторов сравнения `<`, `>`. Таблицу с приоритетом всех операторов вы можете [посмотреть на cppreference.](https://en.cppreference.com/w/cpp/language/operator_precedence) Какое значение сохранится в переменную `x`? {.task_text} ```cpp int x = 16 >> 4; ``` ```consoleoutput {.task_source #cpp_chapter_0023_task_0060} ``` Это деление 16 на 2 в степени 4. {.task_hint} ```cpp {.task_answer} 1 ``` Напоследок рассмотрим два нюанса работы со сдвигами. ### Отбрасывание битов при сдвиге При побитовом сдвиге влево лишние биты **отбрасываются.** Допустим, у нас есть большое число: ``` ┌───────────┬──────────┬──────────┬──────────┬──────────┐ │ Значения │ │ │ │ │ │ байтов │ 01101000 │ 00000000 │ 00000000 │ 00000000 │ ├───────────┼──────────┼──────────┼──────────┼──────────┤ │ Нумерация │ │ │ │ │ │ байтов │ Байт 3 │ Байт 2 │ Байт 1 │ Байт 0 │ └───────────┴──────────┴──────────┴──────────┴──────────┘ ``` Если мы сдвинем его на 1 бит влево, оно увеличится: ``` ┌───────────┬──────────┬──────────┬──────────┬──────────┐ │ Значения │ │ │ │ │ │ байтов │ 11010000 │ 00000000 │ 00000000 │ 00000000 │ ├───────────┼──────────┼──────────┼──────────┼──────────┤ │ Нумерация │ │ │ │ │ │ байтов │ Байт 3 │ Байт 2 │ Байт 1 │ Байт 0 │ └───────────┴──────────┴──────────┴──────────┴──────────┘ ``` Но при дальнейшем сдвиге влево старшие разряды потеряются. Они вытеснятся за границы диапазона числа. И вместо того чтобы расти, число наоборот уменьшится: ``` ┌───────────┬──────────┬──────────┬──────────┬──────────┐ │ Значения │ │ │ │ │ │ байтов │ 10100000 │ 00000000 │ 00000000 │ 00000000 │ ├───────────┼──────────┼──────────┼──────────┼──────────┤ │ Нумерация │ │ │ │ │ │ байтов │ Байт 3 │ Байт 2 │ Байт 1 │ Байт 0 │ └───────────┴──────────┴──────────┴──────────┴──────────┘ ``` Если применить к получившемуся числу сдвиг влево ещё на 3 бита, число превратится в 0. ### Побитовый сдвиг литералов По умолчанию целочисленные литералы имеют тип `int`. И такая запись означает сдвиг значения типа `int` на `n` бит влево: ``` 5 << n ``` Однако у типов `int` и `std::size_t` разный диапазон значений. Побитовый сдвиг `int` на слишком большое значение, адекватное для `std::size_t`, приведёт к ошибке в вычислениях. Если вы хотите применить сдвиг именно к значению `std::size_t`, воспользуйтесь суффиксом `uz`: {#block-uz} ``` 5uz << n ``` [Суффикс uz](https://en.cppreference.com/w/cpp/language/integer_literal) был добавлен в C++23. Он означает, что у литерала тип `std::size_t`. {#block-suffix-uz} Сравните результаты сдвига на 30 бит влево для значений типа `int` и `std::size_t`: ```cpp 3 << 30 // -1073741824 3uz << 30 // 3221225472 ``` Значение типа `int` при побитовом сдвиге влево стало отрицательным. Это произошло из-за способа представления чисел под названием [«Дополнительный код»](https://ru.wikipedia.org/wiki/%D0%94%D0%BE%D0%BF%D0%BE%D0%BB%D0%BD%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9_%D0%BA%D0%BE%D0%B4) (two's complement). Сейчас это не имеет особого значения, но потом пригодится. ## Составное присваивание {#block-compound-assignment} Побитовые операторы относятся к арифметическим операторам. И для них тоже существуют варианты составного присваивания: ```cpp x &= 8; // x = x & 8 x |= 2; // x = x | 2 x ^= 4; // x = x ^ 4 x <<= 2; // x = x << 2 x >>= 1; // x = x >> 1 ``` ## Компактное хранение значений А теперь перейдём к практическому примеру применения побитовых операторов. В Unix-системах любому файлу назначаются права. Они определяют, какие с файлом разрешены действия. Допустимо _любое сочетание_ трёх вариантов доступа: - `r` — чтение, - `w` — запись, - `x` — выполнение. Для хранения этих вариантов требуется ровно 3 бита. Перечислим все возможные комбинации: ``` ┌────────────┬──────────┬────────┐ │ Десятичное │ Двоичное │ Доступ │ │ число │ число │ │ ├────────────┼──────────┼────────┤ │ 0 │ 000 │ Нет │ ├────────────┼──────────┼────────┤ │ 1 │ 001 │ x │ ├────────────┼──────────┼────────┤ │ 2 │ 010 │ w │ ├────────────┼──────────┼────────┤ │ 3 │ 011 │ wx │ ├────────────┼──────────┼────────┤ │ 4 │ 100 │ r │ ├────────────┼──────────┼────────┤ │ 5 │ 101 │ rx │ ├────────────┼──────────┼────────┤ │ 6 │ 110 │ rw │ ├────────────┼──────────┼────────┤ │ 7 │ 111 │ rwx │ └────────────┴──────────┴────────┘ ``` Так, двоичная запись `0b100` означает, что файл доступен только для чтения, а `0b011` — что файл можно записывать и исполнять, но нельзя читать. Права на файл задаются отдельно для: - `u` — пользователя-владельца, - `g` — участников группы-владельца, - `o` — всех остальных. Значит, чтобы описать весь набор прав на файл, потребуется 9 бит. Например, у владельца могут быть полные права на файл, у группы — на чтение и запись, а у остальных — только на чтение: ``` ┌──────────────┬────────┬───────────┐ │ Пользователь │ Группа │ Остальные │ ├──────────────┼────────┼───────────┤ │ 0b111 │ 0b110 │ 0b100 │ └──────────────┴────────┴───────────┘ ``` Если действовать наивно, то для каждого файла на диске придётся хранить 9 целых чисел. Но можно сэкономить и выделить под права только одно число. Покажем, как это работает, на примере типа `int`. Он гарантированно занимает не меньше 2-х байт, то есть 16-ти бит. А значит, его с запасом хватит для хранения 9 бит, описывающих доступ. ```cpp {.example_for_playground .example_for_playground_003} const int user = 7; // 0b111 const int group = 6; // 0b110 const int other = 4; // 0b100 int rights = 0; rights = rights | user; // 0b111 rights = rights << 3; // 0b111'000 rights = rights | group; // 0b111'110 // TODO: добавление прав other ``` Разберём этот код. Мы завели три значения `user`, `group` и `other` и хотим их упаковать в переменную `rights`. Для наглядности мы сначала инициализировали её нулём: все биты числа стали равны нулю. После этого мы применили побитовое «ИЛИ» `rights | user`: ``` 0b000 rights | 0b111 user ----- 0b111 ``` Затем мы сдвинули получившееся значение `rights` на 3 бита влево `rights << 3`: ``` ob111 << 3 == 0b111'000 ``` Тем самым мы освободили крайние 3 бита под запись нового значения `group`: `rights | group`. ``` 0b111'000 rights | 0b000'110 group --------- 0b111'110 ``` Нам осталось по этому же принципу сохранить в переменной `rights` значение `other`. Вынесем этот пример кода в отдельную функцию `set_rights()`. {.task_text} Реализуйте её. Для краткости используйте составное присваивание вместо обычных побитовых операций. Например, `a <<= n` вместо `a = a << n`. {.task_text} ```cpp {.task_source #cpp_chapter_0023_task_0070} int set_rights(int user, int group, int other) { int rights = 0; // Ваш код return rights; } ``` Не забудьте добавить упаковку `other`. {.task_hint} ```cpp {.task_answer} int set_rights(int user, int group, int other) { int rights = user; rights <<= 3; rights |= group; rights <<= 3; rights |= other; return rights; } ``` Мы сохранили три числа в одну переменную. Теперь напишем код, который их восстанавливает. Для этого воспользуемся битовой маской. ## Битовые маски Итак, у нас есть переменная `rights`. Приступим к восстановлению из неё трёх значений: прав пользователя `user`, группы `group` и остальных `other`. Действовать будем в порядке, обратном записи. Сначала прочитаем `other`. Это последние три бита `rights`. Чтобы получить их значение, применим к `rights` побитовый «И» c числом `0b111`. ```cpp int rights = 500; // 0b111'110'100 const int other = rights & 0b111; ``` Мы применили к `rights` битовую маску `0b111`. Битовая маска — это двоичное число для чтения или записи отдельных битов через побитовые операции. Думайте о ней как о трафарете, который накладывается на число, чтобы работать с конкретными битами. Прочтём значения `group` и `user`. Для этого будем сдвигать `rights` на 3 бита вправо и применять к нему битовую маску. ```cpp {.example_for_playground .example_for_playground_005} int rights = 500; // 0b111'110'100 const int other = rights & 0b111; // 0b100 rights >>= 3; // 0b111'110 const int group = rights & 0b111; // 0b110 rights >>= 3; // 0b111 const int user = rights & 0b111; // 0b111 ``` Решите задачу извлечения трёх чисел из переменной `rights`, не изменяя её. Для этого вам нужно составить три разные битовые маски, применить их к `rights`, а результат операции сдвинуть на нужное количество бит. {.task_text} Главное — правильно расставьте скобки: приоритет оператора `>>` выше, чем `&`. Найдите в таблице [на cppreference](https://en.cppreference.com/w/cpp/language/operator_precedence) приоритет всех побитовых операторов. {.task_text} ```cpp {.task_source #cpp_chapter_0023_task_0080} void print_rights(const int rights) { // Ваш код std::println("user={} group={} other={}", user, group, other); } ``` Чтобы получить значение `user`, нужно оператором `&` применить к `rights` битовую маску `0b111'000'000`, а результат сместить на 6 бит вправо. {.task_hint} ```cpp {.task_answer} void print_rights(const int rights) { const int user = (rights & 0b111'000'000) >> 6; const int group = (rights & 0b111'000) >> 3; const int other = rights & 0b111; std::println("user={} group={} other={}", user, group, other); } ``` Хранение прав на файл в Unix-стиле — лишь одно из многих применений побитовых операторов для упаковки значений. Другие классические примеры — хранение трёх компонент цвета в модели RGB или 4-х октетов в IPv4-адресе. ## Примеры побитовых операций Какое значение сохранится в переменную `flags`? Ответ введите в двоичном виде без префикса `0b`. {.task_text} ```cpp {.example_for_playground .example_for_playground_006} int flags = 0b10101; flags |= (1 << 3); ``` ```consoleoutput {.task_source #cpp_chapter_0023_task_0090} ``` Это установка 3-го бита `flags`. Индексация битов идёт справа налево и начинается с нуля. {.task_hint} ```cpp {.task_answer} 11101 ``` Какое значение сохранится в переменную `flags`? Ответ введите в двоичном виде без префикса `0b`. {.task_text} ```cpp {.example_for_playground .example_for_playground_007} int flags = 0b10101; flags &= ~(1 << 2); ``` ```consoleoutput {.task_source #cpp_chapter_0023_task_0100} ``` Это сброс 2-го бита `flags`. Индексация битов идёт справа налево и начинается с нуля. {.task_hint} ```cpp {.task_answer} 10001 ``` Какое значение сохранится в переменную `flags`? Ответ введите в двоичном виде без префикса `0b`. {.task_text} ```cpp {.example_for_playground .example_for_playground_008} int flags = 0b10101; flags ^= (1 << 4); ``` ```consoleoutput {.task_source #cpp_chapter_0023_task_0110} ``` Это инверсия 4-го бита `flags`. Индексация битов идёт справа налево и начинается с нуля. {.task_hint} ```cpp {.task_answer} 101 ``` Напишите функцию, которая проверят, включён ли `n`-ный бит числа `x`. Не забудьте суффикс `uz` для литерала. Индексация битов идёт справа налево и начинается с нуля. {.task_text} Например, вызов `is_on(0b001, 0)` должен вернуть `true`, а вызов `is_on(0b1011, 2)` — `false` {.task_text} ```cpp {.task_source #cpp_chapter_0023_task_0120} bool is_on(std::size_t x, std::size_t n) { } ``` Примените битовую маску: бит `1`, сдвинутый на `n` позиций влево. Бит установлен в 1, если побитовый «И» числа с этой маской равен `1`.{.task_hint} ```cpp {.task_answer} bool is_on(std::size_t x, std::size_t n) { return x & (1uz << n); } ``` ---------- ## Резюме - Побитовые операторы (bitwise operators) позволяют работать с _отдельными битами_ целых чисел. - Побитовые операторы: - `~` — инверсия бита, - `&` — «И», - `|` — «ИЛИ», - `^` — XOR, - `<<` — побитовый сдвиг вправо, - `>>` — побитовый сдвиг влево. - Операция `x << n` равносильна умножению `x` на 2 в степени `n`. - Операция `x >> n` равносильна целочисленному делению `x` на 2 в степени `n`. - Битовые маски применяются в сочетании с побитовыми операторами для доступа к конкретным битам.
Отправка...
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!