O.3 – Битовые манипуляции с побитовыми операторами и битовыми масками

Добавлено 9 мая 2021 в 13:07

В предыдущем уроке о побитовых операторах (O.2 – Побитовые операторы) мы обсудили, как различные побитовые операторы применяют логические операции к каждому биту в операндах. Теперь, когда мы понимаем, как они работают, давайте посмотрим, как они чаще всего используются.

Битовые маски

Чтобы управлять отдельными битами (например, устанавливать их в 1 или сбрасывать в 0), нам нужен способ идентифицировать конкретные биты, которыми мы хотим манипулировать. К сожалению, побитовые операторы не умеют работать с битовыми позициями. Вместо этого они работают с битовыми масками.

Битовая маска – это предопределенный набор битов, который используется для выбора того, какие конкретные биты будут изменены при последующих операциях.

Рассмотрим случай из реальной жизни, когда вы хотите покрасить оконную раму. Если не проявить осторожность, вы рискуете покрасить не только оконную раму, но и само стекло. Вы можете купить малярный скотч и приклеить его к стеклу и другим частям, которые не нужно красить. Затем, когда вы будете красить, малярный скотч будет блокировать попадание краски на всё, что вы не хотите красить. В конце концов, окрашиваются только немаскированные части (те части, которые вы хотите покрасить).

Битовая маска, по сути, выполняет ту же функцию для битов – битовая маска блокирует побитовые операторы от прикосновения к битам, которые мы не хотим изменять, и позволяет получить доступ к тем, которые мы действительно хотим изменить.

Давайте сначала узнаем, как определять простые битовые маски, а затем мы покажем, как их использовать.

Определение битовых масок в C++14

Самый простой набор битовых масок – это определение одной битовой маски для каждой битовой позиции. Мы используем нули, чтобы замаскировать биты, которые нам не нужны, и единицы, чтобы обозначить биты, которые мы хотим изменить.

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

Поскольку C++14 поддерживает двоичные литералы, определить эти битовые маски очень просто:

#include <cstdint>
 
constexpr std::uint_fast8_t mask0{ 0b0000'0001 }; // представляет бит 0
constexpr std::uint_fast8_t mask1{ 0b0000'0010 }; // представляет бит 1
constexpr std::uint_fast8_t mask2{ 0b0000'0100 }; // представляет бит 2 
constexpr std::uint_fast8_t mask3{ 0b0000'1000 }; // представляет бит 3
constexpr std::uint_fast8_t mask4{ 0b0001'0000 }; // представляет бит 4
constexpr std::uint_fast8_t mask5{ 0b0010'0000 }; // представляет бит 5
constexpr std::uint_fast8_t mask6{ 0b0100'0000 }; // представляет бит 6
constexpr std::uint_fast8_t mask7{ 0b1000'0000 }; // представляет бит 7

Теперь у нас есть набор символьных констант, представляющих каждую битовую позицию. Мы можем использовать их для манипулирования битами (вскоре мы покажем, как это сделать).

Определение битовых масок в C++11 или в более ранних версиях

Поскольку C++11 не поддерживает двоичные литералы, то для установки символьных констант мы должны использовать другие методы. Для этого есть два хороших способа. Менее понятным, но более распространенным является использование шестнадцатеричных чисел. Если вам нужно напомнить о шестнадцатеричной системе счисления, еще раз посмотрите урок «4.13 – Литералы».

constexpr std::uint_fast8_t mask0{ 0x1 };  // шестнадцатеричная форма для 0000 0001 
constexpr std::uint_fast8_t mask1{ 0x2 };  // шестнадцатеричная форма для 0000 0010
constexpr std::uint_fast8_t mask2{ 0x4 };  // шестнадцатеричная форма для 0000 0100
constexpr std::uint_fast8_t mask3{ 0x8 };  // шестнадцатеричная форма для 0000 1000
constexpr std::uint_fast8_t mask4{ 0x10 }; // шестнадцатеричная форма для 0001 0000
constexpr std::uint_fast8_t mask5{ 0x20 }; // шестнадцатеричная форма для 0010 0000
constexpr std::uint_fast8_t mask6{ 0x40 }; // шестнадцатеричная форма для 0100 0000
constexpr std::uint_fast8_t mask7{ 0x80 }; // шестнадцатеричная форма для 1000 0000

Это может быть трудно читать. Один из способов упростить задачу – использовать оператор сдвига влево, чтобы сместить бит в нужное место:

constexpr std::uint_fast8_t mask0{ 1 << 0 }; // 0000 0001 
constexpr std::uint_fast8_t mask1{ 1 << 1 }; // 0000 0010
constexpr std::uint_fast8_t mask2{ 1 << 2 }; // 0000 0100
constexpr std::uint_fast8_t mask3{ 1 << 3 }; // 0000 1000
constexpr std::uint_fast8_t mask4{ 1 << 4 }; // 0001 0000
constexpr std::uint_fast8_t mask5{ 1 << 5 }; // 0010 0000
constexpr std::uint_fast8_t mask6{ 1 << 6 }; // 0100 0000
constexpr std::uint_fast8_t mask7{ 1 << 7 }; // 1000 0000

Проверка бита (чтобы узнать, установлен ли он в 1, или нет)

Теперь, когда у нас есть набор битовых масок, мы можем использовать их вместе с переменной битовых флагов для управления этими битовыми флагами.

Чтобы определить, установлен ли бит в 1 (включен, on) или сброшен в 0 (выключен, off), мы используем побитовое И в сочетании с битовой маской для соответствующего бита:

#include <cstdint>
#include <iostream>
 
int main()
{
    constexpr std::uint_fast8_t mask0{ 0b0000'0001 }; // представляет бит 0
    constexpr std::uint_fast8_t mask1{ 0b0000'0010 }; // представляет бит 1
    constexpr std::uint_fast8_t mask2{ 0b0000'0100 }; // представляет бит 2 
    constexpr std::uint_fast8_t mask3{ 0b0000'1000 }; // представляет бит 3
    constexpr std::uint_fast8_t mask4{ 0b0001'0000 }; // представляет бит 4
    constexpr std::uint_fast8_t mask5{ 0b0010'0000 }; // представляет бит 5
    constexpr std::uint_fast8_t mask6{ 0b0100'0000 }; // представляет бит 6
    constexpr std::uint_fast8_t mask7{ 0b1000'0000 }; // представляет бит 7
 
    std::uint_fast8_t flags{ 0b0000'0101 }; // 8 бит в размере означает место для 8 флагов
 
    std::cout << "bit 0 is " << ((flags & mask0) ? "on\n" : "off\n");
    std::cout << "bit 1 is " << ((flags & mask1) ? "on\n" : "off\n");
 
    return 0;
}

Эта программа напечатает:

bit 0 is on
bit 1 is off

Установка бита в 1

Чтобы установить бит в 1, мы используем присваивание с побитовым ИЛИ (оператор |=) в сочетании с битовой маской для соответствующего бита:

#include <cstdint>
#include <iostream>
 
int main()
{
    constexpr std::uint_fast8_t mask0{ 0b0000'0001 }; // представляет бит 0
    constexpr std::uint_fast8_t mask1{ 0b0000'0010 }; // представляет бит 1
    constexpr std::uint_fast8_t mask2{ 0b0000'0100 }; // представляет бит 2 
    constexpr std::uint_fast8_t mask3{ 0b0000'1000 }; // представляет бит 3
    constexpr std::uint_fast8_t mask4{ 0b0001'0000 }; // представляет бит 4
    constexpr std::uint_fast8_t mask5{ 0b0010'0000 }; // представляет бит 5
    constexpr std::uint_fast8_t mask6{ 0b0100'0000 }; // представляет бит 6
    constexpr std::uint_fast8_t mask7{ 0b1000'0000 }; // представляет бит 7
 
    std::uint_fast8_t flags{ 0b0000'0101 }; // 8 бит в размере означает место для 8 флагов
 
    std::cout << "bit 1 is " << ((flags & mask1) ? "on\n" : "off\n");
 
    flags |= mask1; // установить в 1 (включить) бит 1
 
    std::cout << "bit 1 is " << ((flags & mask1) ? "on\n" : "off\n");
 
    return 0;
}

Эта программа напечатает:

bit 1 is off
bit 1 is on

Мы также можем установить в 1 несколько бит одновременно, используя побитовое ИЛИ:

flags |= (mask4 | mask5); // устанавливаем биты 4 и 5 одновременно

Сброс бита в 0

Чтобы сбросить бит в 0, мы используем побитовое И и побитовое НЕ вместе:

#include <cstdint>
#include <iostream>
 
int main()
{
    constexpr std::uint_fast8_t mask0{ 0b0000'0001 }; // представляет бит 0
    constexpr std::uint_fast8_t mask1{ 0b0000'0010 }; // представляет бит 1
    constexpr std::uint_fast8_t mask2{ 0b0000'0100 }; // представляет бит 2 
    constexpr std::uint_fast8_t mask3{ 0b0000'1000 }; // представляет бит 3
    constexpr std::uint_fast8_t mask4{ 0b0001'0000 }; // представляет бит 4
    constexpr std::uint_fast8_t mask5{ 0b0010'0000 }; // представляет бит 5
    constexpr std::uint_fast8_t mask6{ 0b0100'0000 }; // представляет бит 6
    constexpr std::uint_fast8_t mask7{ 0b1000'0000 }; // представляет бит 7
 
    std::uint_fast8_t flags{ 0b0000'0101 }; // 8 бит в размере означает место для 8 флагов
 
    std::cout << "bit 2 is " << ((flags & mask2) ? "on\n" : "off\n");
 
    flags &= ~mask2; // сбросить в 0 (выключить) бит 2
 
    std::cout << "bit 2 is " << ((flags & mask2) ? "on\n" : "off\n");
 
    return 0;
}

Эта программа напечатает:

bit 2 is on
bit 2 is off

Мы можем сбросить в 0 несколько бит одновременно:

flags &= ~(mask4 | mask5); // одновременно сбрасываем в 0 биты 4 и 5

Инвертирование бита

Чтобы инвертировать (переключить на противоположное) состояние бита, мы используем побитовое исключающее ИЛИ:

#include <cstdint>
#include <iostream>
 
int main()
{
    constexpr std::uint_fast8_t mask0{ 0b0000'0001 }; // представляет бит 0
    constexpr std::uint_fast8_t mask1{ 0b0000'0010 }; // представляет бит 1
    constexpr std::uint_fast8_t mask2{ 0b0000'0100 }; // представляет бит 2 
    constexpr std::uint_fast8_t mask3{ 0b0000'1000 }; // представляет бит 3
    constexpr std::uint_fast8_t mask4{ 0b0001'0000 }; // представляет бит 4
    constexpr std::uint_fast8_t mask5{ 0b0010'0000 }; // представляет бит 5
    constexpr std::uint_fast8_t mask6{ 0b0100'0000 }; // представляет бит 6
    constexpr std::uint_fast8_t mask7{ 0b1000'0000 }; // представляет бит 7
 
    std::uint_fast8_t flags{ 0b0000'0101 }; // 8 бит в размере означает место для 8 флагов
 
    std::cout << "bit 2 is " << ((flags & mask2) ? "on\n" : "off\n");
    flags ^= mask2; // инвертирует бит 2
    std::cout << "bit 2 is " << ((flags & mask2) ? "on\n" : "off\n");
    flags ^= mask2; // инвертирует бит 2
    std::cout << "bit 2 is " << ((flags & mask2) ? "on\n" : "off\n");
 
    return 0;
}

Эта программа напечатает:

bit 2 is on
bit 2 is off
bit 2 is on

Мы можем инвертировать несколько бит одновременно:

flags ^= (mask4 | mask5); // инвертируем биты 4 и 5 одновременно

Битовые маски и std::bitset

std::bitset поддерживает полный набор побитовых операторов. Таким образом, несмотря на то, что для изменения отдельных битов проще использовать функции (test, set, reset и flip), если хотите, вы можете использовать побитовые операторы и битовые маски.

Зачем это нужно? Функции позволяют изменять только отдельные биты. Побитовые операторы позволяют изменять сразу несколько битов.

#include <cstdint>
#include <iostream>
#include <bitset>
 
int main()
{
    constexpr std::uint_fast8_t mask0{ 0b0000'0001 }; // представляет бит 0
    constexpr std::uint_fast8_t mask1{ 0b0000'0010 }; // представляет бит 1
    constexpr std::uint_fast8_t mask2{ 0b0000'0100 }; // представляет бит 2 
    constexpr std::uint_fast8_t mask3{ 0b0000'1000 }; // представляет бит 3
    constexpr std::uint_fast8_t mask4{ 0b0001'0000 }; // представляет бит 4
    constexpr std::uint_fast8_t mask5{ 0b0010'0000 }; // представляет бит 5
    constexpr std::uint_fast8_t mask6{ 0b0100'0000 }; // представляет бит 6
    constexpr std::uint_fast8_t mask7{ 0b1000'0000 }; // представляет бит 7
 
    std::bitset<8> flags{ 0b0000'0101 }; // 8 бит в размере означает место для 8 флагов
 
    std::cout << "bit 1 is " << (flags.test(1) ? "on\n" : "off\n");
    std::cout << "bit 2 is " << (flags.test(2) ? "on\n" : "off\n");
    
    flags ^= (mask1 | mask2); // инвертирует биты 1 и 2
 
    std::cout << "bit 1 is " << (flags.test(1) ? "on\n" : "off\n");
    std::cout << "bit 2 is " << (flags.test(2) ? "on\n" : "off\n");
    
    flags |= (mask1 | mask2); // устанавливает в 1 биты 1 и 2
 
    std::cout << "bit 1 is " << (flags.test(1) ? "on\n" : "off\n");
    std::cout << "bit 2 is " << (flags.test(2) ? "on\n" : "off\n");
    
    flags &= ~(mask1 | mask2); // сбрасывает в 0 биты 1 и 2
 
    std::cout << "bit 1 is " << (flags.test(1) ? "on\n" : "off\n");
    std::cout << "bit 2 is " << (flags.test(2) ? "on\n" : "off\n");
 
    return 0;
}

Эта программа напечатает:

bit 1 is off
bit 2 is on
bit 1 is on
bit 2 is off
bit 1 is on
bit 2 is on
bit 1 is off
bit 2 is off

Делаем битовые маски более осмысленными

Присвоение нашим битовым маскам имен "mask1" или "mask2" говорит нам, какой бит обрабатывается, но не дает нам никакой индикации, для чего на самом деле используется этот битовый флаг.

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

#include <cstdint>
#include <iostream>
 
int main()
{
    // Определяем набор физических/эмоциональных состояний
    constexpr std::uint_fast8_t isHungry{    1 << 0 }; // 0000 0001
    constexpr std::uint_fast8_t isSad{        1 << 1 }; // 0000 0010
    constexpr std::uint_fast8_t isMad{        1 << 2 }; // 0000 0100
    constexpr std::uint_fast8_t isHappy{    1 << 3 }; // 0000 1000
    constexpr std::uint_fast8_t isLaughing{ 1 << 4 }; // 0001 0000
    constexpr std::uint_fast8_t isAsleep{    1 << 5 }; // 0010 0000
    constexpr std::uint_fast8_t isDead{        1 << 6 }; // 0100 0000
    constexpr std::uint_fast8_t isCrying{    1 << 7 }; // 1000 0000
 
    std::uint_fast8_t me{};       // все флаги/параметры сброшены при запуске
    me |= (isHappy | isLaughing); // я счастлив и смеюсь
    me &= ~isLaughing;            // я больше не смеюсь
 
    // Запрос нескольких состояний
    // (мы будем использовать static_cast<bool>, чтобы 
    // интерпретировать результаты как логическое значение)
    std::cout << "I am happy? " << static_cast<bool>(me & isHappy) << '\n';
    std::cout << "I am laughing? " << static_cast<bool>(me & isLaughing) << '\n';
 
    return 0;
}

Вот тот же пример, реализованный с использованием std::bitset:

#include <iostream>
#include <bitset>
 
int main()
{
    // Определяем набор физических/эмоциональных состояний
    std::bitset<8> isHungry{    0b0000'0001 };
    std::bitset<8> isSad{        0b0000'0010 };
    std::bitset<8> isMad{        0b0000'0100 };
    std::bitset<8> isHappy{        0b0000'1000 };
    std::bitset<8> isLaughing{    0b0001'0000 };
    std::bitset<8> isAsleep{    0b0010'0000 };
    std::bitset<8> isDead{        0b0100'0000 };
    std::bitset<8> isCrying{    0b1000'0000 };
 
 
    std::bitset<8> me{};          // все флаги/параметры сброшены при запуске
    me |= (isHappy | isLaughing); // я счастлив и смеюсь
    me &= ~isLaughing;            // я больше не смеюсь
 
    // Запрос нескольких состояний (мы используем функцию any(), чтобы увидеть, 
    // остались ли какие-либо биты установленными)
    std::cout << "I am happy? " << (me & isHappy).any() << '\n';
    std::cout << "I am laughing? " << (me & isLaughing).any() << '\n';
 
    return 0;
}

Два примечания: во-первых, std::bitset не имеет удобной функции, позволяющей запрашивать биты с использованием битовой маски. Поэтому, если вы хотите использовать битовые маски, а не индексы позиций, для запроса битов вам придется использовать побитовое И. Во-вторых, чтобы увидеть, остался ли запрошенный нами бит установленным или сброшенным, мы используем функцию any(), которая возвращает true, если какие-либо биты установлены в 1, и false в противном случае.

Когда битовые флаги наиболее полезны?

Проницательные читатели могут заметить, что приведенные выше примеры на самом деле не экономят память. 8 логических значений обычно занимают 8 байтов. Но в приведенных выше примерах используется 9 байтов (8 байтов для определения битовых масок и 1 байт для переменной флага)!

Битовые флаги имеют наибольший смысл, когда у вас много идентичных переменных-флагов. Например, в приведенном выше примере представьте, что вместо одного человека (me) у вас было бы 100. Если вы использовали 8 логических значений на человека (по одному для каждого возможного состояния), вы использовали бы 800 байт памяти. С битовыми флагами вы должны использовать 8 байт для битовых масок и 100 байт для переменных битовых флагов, в общей сложности 108 байт памяти – примерно в 8 раз меньше памяти.

Для большинства программ объем памяти, сэкономленный с помощью битовых флагов, не стоит дополнительного усложнения. Но в программах, где есть десятки тысяч или даже миллионы похожих объектов, использование битовых флагов может существенно сократить использование памяти. Это полезный способ оптимизации, если она вам понадобится.

Есть еще один случай, когда использование битовых флагов и битовых масок может иметь смысл. Представьте, что у вас есть функция, которая может принимать любую комбинацию из 32 различных параметров. Один из способов написать эту функцию – использовать 32 отдельных логических параметра:

void someFunction(bool option1, bool option2, bool option3, bool option4, bool option5, 
                  bool option6, bool option7, bool option8, bool option9, bool option10, 
                  bool option11, bool option12, bool option13, bool option14, bool option15, 
                  bool option16, bool option17, bool option18, bool option19, bool option20, 
                  bool option21, bool option22, bool option23, bool option24, bool option25, 
                  bool option26, bool option27, bool option28, bool option29, bool option30, 
                  bool option31, bool option32);

Надеюсь, вы дадите своим параметрам более информативные имена, но суть здесь в том, чтобы показать вам, насколько плох длинный список параметров.

Затем, когда вы захотите вызвать функцию с параметрами 10 и 32, установленными в значение true, вам нужно будет сделать это следующим образом:

someFunction(false, false, false, false, false, false, false, false, false, true, 
             false, false, false, false, false, false, false, false, false, false, 
             false, false, false, false, false, false, false, false, false, false, 
             false, true);

Это до смешного сложно читать (это параметр 9, 10 или 11 установлен в значение true?), а также это означает, что вы должны помнить, какой аргумент соответствует какому параметру («флаг редактирования» устанавливается 9-ым, 10-ым или 11-ым параметром?). Это также может быть не очень производительным, поскольку каждый вызов функции должен копировать 32 логических значения из вызывающей функции в вызываемую функцию.

Если вместо этого вы определили функцию, используя битовые флаги, например:

void someFunction(std::bitset<32> options);

Вы сможете использовать битовые флаги для передачи только тех параметров, которые вам нужны:

someFunction(option10 | option32);

Это не только намного удобнее для чтения, но и, вероятно, будет более производительным, поскольку включает всего 2 операции (одно побитовое ИЛИ и одно копирование параметра).

Это одна из причин, по которой OpenGL, хорошо зарекомендовавшая себя библиотека трехмерной графики, решила использовать параметры битовых флагов вместо последовательностей множества логических параметров.

Вот пример вызова функции из OpenGL:

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // очищаем буфер цвета и глубины

GL_COLOR_BUFFER_BIT и GL_DEPTH_BUFFER_BIT – это битовые маски, определенные следующим образом (в gl2.h):

#define GL_DEPTH_BUFFER_BIT               0x00000100
#define GL_STENCIL_BUFFER_BIT             0x00000400
#define GL_COLOR_BUFFER_BIT               0x00004000

Битовые маски, включающие несколько битов

Хотя битовые маски часто используются для выбора одного бита, их также можно использовать для выбора нескольких битов. Давайте посмотрим на немного более сложный пример, в котором мы это делаем.

Цветные дисплеи, такие как телевизоры и мониторы, состоят из миллионов пикселей, каждый из которых может отображать точку цвета. Точка цвета состоит из трех световых лучей: красного, зеленого и синего (RGB – red, green, blue). Изменяя интенсивность этих цветов, можно получить любой цвет в цветовом спектре. Обычно яркость R, G и B для пикселя представлена 8-битовым целочисленным типом без знака. Например, красный пиксель будет иметь R = 255, G = 0, B = 0. У фиолетового пикселя R = 255, G = 0, B = 255. Средне-серый пиксель будет иметь R = 127, G = 127, B = 127.

При присвоении значений цвета пикселю, помимо R, G и B, часто используется 4-е значение, называемое A. «A» означает «альфа», и оно определяет, насколько прозрачным будет цвет. Если A = 0, цвет полностью прозрачный. Если A = 255, цвет непрозрачный.

R, G, B и A обычно хранятся как одно 32-битное целое число, в котором для каждого компонента используется 8 бит:

32-битное значение RGBA
биты 31-24биты 23-16биты 15-8биты 7-0
RRRRRRRRGGGGGGGGBBBBBBBBAAAAAAAA
красныйзеленыйсинийальфа

Следующая программа просит пользователя ввести 32-битное шестнадцатеричное значение, а затем извлекает из него 8-битные цветовые значения для R, G, B и A.

#include <cstdint>
#include <iostream>
 
int main()
{
    constexpr std::uint_fast32_t redBits{ 0xFF000000 };
    constexpr std::uint_fast32_t greenBits{ 0x00FF0000 };
    constexpr std::uint_fast32_t blueBits{ 0x0000FF00 };
    constexpr std::uint_fast32_t alphaBits{ 0x000000FF };
 
    std::cout << "Enter a 32-bit RGBA color value in hexadecimal (e.g. FF7F3300): ";
    std::uint_fast32_t pixel{};
    std::cin >> std::hex >> pixel; // std::hex позволяет нам читать в шестнадцатеричном формате
 
    // используем побитовое И, чтобы изолировать значение красного цвета,
    // затем сдвигаем вправо значение в младшие 8 бит
    std::uint_fast8_t red{ static_cast<std::uint_fast8_t>((pixel & redBits) >> 24) };
    std::uint_fast8_t green{ static_cast<std::uint_fast8_t>((pixel & greenBits) >> 16) };
    std::uint_fast8_t blue{ static_cast<std::uint_fast8_t>((pixel & blueBits) >> 8) };
    std::uint_fast8_t alpha{ static_cast<std::uint_fast8_t>(pixel & alphaBits) };
 
    std::cout << "Your color contains:\n";
    std::cout << std::hex; // print the following values in hex
    std::cout << static_cast<int>(red) << " red\n";
    std::cout << static_cast<int>(green) << " green\n";
    std::cout << static_cast<int>(blue) << " blue\n";
    std::cout << static_cast<int>(alpha) << " alpha\n";
 
    return 0;
}

Эта программа дает следующий результат:

Enter a 32-bit RGBA color value in hexadecimal (e.g. FF7F3300): FF7F3300
Your color contains:
ff red
7f green
33 blue
0 alpha

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

Резюме

Обобщим, как устанавливать, сбрасывать, инвертировать и запрашивать битовые флаги:

Чтобы запросить состояния битов, мы используем побитовое И:

if (flags & option4) ... // если установлен option4, сделать что-то

Для установки битов в 1 (включения) используем побитовое ИЛИ:

flags |= option4;             // включаем опцию 4.
flags |= (option4 | option5); // включаем опции 4 м 5.

Чтобы сбросить биты в 0 (очистить, выключить), мы используем побитовое И с побитовым НЕ:

flags &= ~option4;             // отключаем опцию 4
flags &= ~(option4 | option5); // отключаем опции 4 и 5

Чтобы инвертировать состояния битов, мы используем побитовое исключающее ИЛИ:

flags ^= option4;             // переключаем option4 с 1 на 0, или наоборот
flags ^= (option4 | option5); // инвертируем опции 4 и 5

Небольшой тест

Вопрос 1

В этом тесте не используйте std::bitset. Мы используем std::bitset только для печати.

Для заданной программы:

#include <bitset>
#include <cstdint>
#include <iostream>
 
int main()
{
    constexpr std::uint_fast8_t option_viewed{ 0x01 };
    constexpr std::uint_fast8_t option_edited{ 0x02 };
    constexpr std::uint_fast8_t option_favorited{ 0x04 };
    constexpr std::uint_fast8_t option_shared{ 0x08 };
    constexpr std::uint_fast8_t option_deleted{ 0x10 };
 
    std::uint_fast8_t myArticleFlags{ option_favorited };
 
    // ...
 
    std::cout << std::bitset<8>{ myArticleFlags } << '\n';
 
    return 0;
}

a) Напишите строку кода, чтобы сделать статью просматриваемой (включить флаг option_viewed).

Ожидаемый вывод:

00000101

myArticleFlags |= option_viewed;

b) Напишите строку кода, чтобы проверить, была ли удалена статья (флаг option_deleted).

if (myArticleFlags & option_deleted) ...

c) Напишите строку кода, чтобы сбросить у статьи статус избранная (сбросить флаг option_favorited).

Ожидаемый результат (при условии, что вы выполнили задание (а):

00000001

myArticleFlags &= ~option_favorited;

Вопрос 2

Дополнительное задание: почему следующие две строки идентичны?

myflags &= ~(option4 | option5); // выключает опции 4 и 5
myflags &= ~option4 & ~option5;  // выключает опции 4 и 5

Закон Де Моргана гласит, что если мы распространяем НЕ, нам нужно поменять ИЛИ на И, и наоборот. Поэтому, ~(option4 | option5) становится ~option4 & ~option5.

Теги

C++ / CppLearnCppstd::bitsetБитовая маскаБитовые манипуляцииБитовый флагДля начинающихОбучениеОператор (программирование)Программирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.