Урок №46. Битовые флаги и битовые маски

  Юрий  | 

  |

  Обновл. 11 Сен 2021  | 

 119853

 ǀ   40 

На этом уроке мы рассмотрим битовые флаги и битовые маски в языке С++.

Примечание: Для некоторых этот материал может показаться немного сложным. Если вы застряли или что-то не понятно — пропустите этот урок, в будущем сможете вернуться и разобраться детально. Он не столь важен для прогресса в изучении языка C++, как другие уроки, и изложен здесь в большей мере для общего развития.

Битовые флаги

Используя целый байт для хранения значения логического типа данных, вы занимаете только 1 бит, а остальные 7 из 8 — не используются. Хотя в целом это нормально, но в особых, ресурсоёмких случаях, связанных с множеством логических значений, может быть полезно «упаковать» 8 значений типа bool в 1 байт, сэкономив при этом память и увеличив, таким образом, производительность. Эти отдельные биты и называются битовыми флагами. Поскольку доступ к этим битам напрямую отсутствует, то для операций с ними используются побитовые операторы.

Примечание: На этом уроке мы будем использовать значения из шестнадцатеричной системы счисления.

Например:

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

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

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

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

В качестве примера возьмем библиотеку 3D-графики OpenGL, в которой некоторые функции принимают один или несколько битовых флагов в качестве параметров:

GL_COLOR_BUFFER_BIT и GL_DEPTH_BUFFER_BIT определяются следующим образом (в gl2.h):

Вот небольшой пример:

Почему битовые флаги полезны?


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

Они используются в двух случаях:

Cлучай №1: Если у вас много идентичных битовых флагов.

Вместо одной переменной myflags, рассмотрим случай, когда у вас есть две переменные: myflags1 и myflags2, каждая из которых может хранить 8 значений. Если вы определите их как два отдельных логических набора, то вам потребуется 16 логических значений и, таким образом, 16 байт. Однако с использованием битовых флагов вам потребуется только 10 байт (8 для определения параметров и 1 для каждой переменной myflags). А вот если у вас будет 100 переменных myflags, то, используя битовые флаги, вам потребуется 108 байт вместо 800. Чем больше идентичных переменных вам нужно, тем более значительной будет экономия памяти.

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

Чтобы отследить, к какому типу атаки монстр устойчив, мы можем использовать одно логическое значение на сопротивление (для одного монстра). Это 8 логических значений (сопротивлений) для одного монстра = 8 байт.

Для 100 монстров это будет 800 переменных типа bool и 800 байт памяти.

А вот используя битовые флаги:

Нам нужен будет только 1 байт для хранения сопротивления каждого монстра и одноразовая плата в 8 байт для типов атак.

Таким образом, потребуется только 108 байт или примерно в 8 раз меньше памяти.

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

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

Затем, если вы захотите вызвать функцию с 10-м и 32-м параметрами, установленными как true — вам придется сделать следующее:

Т.е. перечислить все варианты как false, кроме 10 и 32 — они true. Читать такой код сложно, да и требуется держать в памяти порядковые номера нужных параметров (10 и 32 или 11 и 33?). Такая простыня не может быть эффективной.

А вот если определить функцию с помощью битовых флагов:

То можно выбирать и передавать только нужные параметры:

Кроме того, что это читабельнее, это также эффективнее и производительнее, так как включает только 2 операции (1 побитовое ИЛИ и 1 передача параметров).

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

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

Введение в std::bitset

Все эти биты, битовые флаги, операции-манипуляции — всё это утомительно, не правда ли? К счастью, в Стандартной библиотеке C++ есть такой объект, как std::bitset, который упрощает работу с битовыми флагами.

Для его использования необходимо подключить заголовочный файл bitset, а затем определить переменную типа std::bitset, указав необходимое количество бит. Она должна быть константой типа compile time.

При желании std::bitset можно инициализировать начальным набором значений:

Обратите внимание, наше начальное значение конвертируется в двоичную систему. Так как мы ввели шестнадцатеричное 2, то std::bitset преобразует его в двоичное 0000 0010.

В std::bitset есть 4 основные функции:

   функция test() — позволяет узнать значение бита (0 или 1).

   функция set() — позволяет включить биты (если они уже включены, то ничего не произойдет).

   функция reset() — позволяет выключить биты (если они уже выключены, то ничего не произойдет).

   функция flip() — позволяет изменить значения бит на противоположные (с 0 на 1 или с 1 на 0).

Каждая из этих функций принимает в качестве параметров позиции бит. Позиция крайнего правого бита (последнего) равна 0, затем порядковый номер растет с каждым последующим битом влево (1, 2, 3, 4 и т.д.). Старайтесь давать содержательные имена битовым индексам (либо путем присваивания их константным переменным, либо с помощью перечислений — о них мы поговорим на соответствующем уроке).

Результат выполнения программы:

Bit 4 has value: 1
Bit 5 has value: 0
All the bits: 00010010

Обратите внимание, отправляя переменную bits в std::cout — выводятся значения всех бит в std::bitset.

Помните, что инициализируемое значение std::bitset рассматривается как двоичное, в то время как функции std::bitset используют позиции бит!

std::bitset также поддерживает стандартные побитовые операторы (|, & и ^), которые также можно использовать (они полезны при проведении операций одновременно сразу с несколькими битами).

Вместо выполнения всех побитовых операций вручную, рекомендуется использовать std::bitset, так как он более удобен и менее подвержен ошибкам.

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


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

Рассмотрим пример. В следующей программе мы просим пользователя ввести число. Затем, используя битовую маску, мы сохраняем только последние 4 бита, значения которых и выводим в консоль:

Результат выполнения программы:

Enter an integer: 151
The 4 low bits have value: 7

151 в десятичной системе равно 1001 0111 в двоичной. lowMask — это 0000 1111 в 8-битной двоичной системе. 1001 0111 & 0000 1111 = 0000 0111, что равно десятичному 7.

Пример с RGBA

Цветные дисплейные устройства, такие как телевизоры и мониторы, состоят из миллионов пикселей, каждый из которых может отображать точку цвета. Точка цвета состоит из 3 пучков: один красный, один зелёный и один синий (сокр. «RGB» от англ. «Red, Green, Blue»). Изменяя их интенсивность, можно воссоздать любой цвет. Количество цветов R, G и В в одном пикселе представлено 8-битным целым числом unsigned. Например, красный цвет имеет R = 255, G = 0, B = 0; фиолетовый: R = 255, G = 0, B = 255; серый: R = 127, G = 127, B = 127.

Используется еще 4-е значение, которое называется А. «А» от англ. «Alfa», которое отвечает за прозрачность. Если А = 0, то цвет полностью прозрачный. Если А = 255, то цвет непрозрачный.

В совокупности R, G, В и А составляют одно 32-битное целое число, с 8 битами для каждого компонента:

32-битное значение RGBA
31-24 бита 23-16 бит 15-8 бит 7-0 бит
RRRRRRRR GGGGGGGG BBBBBBBB AAAAAAAA
red green blue alpha

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

Результат выполнения программы:

Enter a 32-bit RGBA color value in hexadecimal (e.g. FF7F3300): FF7F3300
Your color contains:
255 of 255 red
127 of 255 green
51 of 255 blue
0 of 255 alpha

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

Примечание: RGBA иногда может храниться как ARGB. В таком случае, главным байтом является альфа.

Заключение


Давайте кратко повторим то, как включать, выключать, переключать и запрашивать битовые флаги.

Для запроса битового состояния используется побитовое И:

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

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

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

Тест

Есть следующий фрагмент кода:

Примечание: Статья — это myArticleFlags.

Задание №1

Добавьте строку кода, чтобы пометить статью как уже прочитанную (option_viewed).

Задание №2

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

Задание №3

Добавьте строку кода, чтобы открепить статью от закрепленного места (option_favorited).

Задание №4

Почему следующие две строки идентичны?

Ответы

Ответ №1

myArticleFlags |= option_viewed;

Ответ №2

if (myArticleFlags & option_deleted) …

Ответ №3

myArticleFlags &= ~option_favorited;

Ответ №4

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

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (345 оценок, среднее: 4,80 из 5)
Загрузка...

Комментариев: 40

  1. AdamZakaev:

    Байты сдвигаются вправо чтобы диапазон значений был 0-255 если этого не сделать то значение не поместится в int и выведет нуль т.к произойдёт переполнение и байты просто потеряются

  2. Finchi:

    В примере с RGBA не обязательно использовать битовые маски, достаточно простого сдвига ">>" , если переменная pixel нам больше не нужна. Если нужна, то можно сохранить ее значение в другой переменной.

  3. Surprizze:

    Добрые люди, объясните пожалуйста строчки из статьи:

    Зачем нужно сдвигать биты вправо на 24? То есть FF000000 в двоичной 11111111 00000000 00000000 00000000(Разделил по 8 для удобного подсчета)
    Мы сдвигаем 11111111 за пределы числа? Для чего? То есть после сдвига биты теряются навсегда и получится
    00000000 00000000 00000000
    так ведь?
    Ну ладно , даже если число останется 32 битным по-прежнему будет так:
    00000000 00000000 00000000 11111111
    но ведь в следующей строке это:
    unsigned char green = (pixel & greenBits) >> 16;
    получается:
    11111111 00000000 00000000 11111111
    сдвигаемся на 16
    и выходит единицы в единицах?(Да звучит странно)
    Объясните пожалуйста , я правда не могу понять.

    1. серик:

      "Enter a 32-bit RGBA color value in hexadecimal (e.g. FF7F3300): FF7F3300
      Your color contains:
      255 of 255 red
      127 of 255 green
      51 of 255 blue
      0 of 255 alpha"
      Из ответа можно догадаться в чём смысл "сдвига вправо".
      Мы видим интенсивность цветов красного(255 из 255) зелёного(127 из 255)
      синего(51 из 255) и прозрачность(0 из 255).

    2. Илья:

      В данном случае pixel  не затирается новым значением, поэтому биты не затрутся, но на каждом шаге мы получим биты из нужной (незамаскарированной) группы, которые сдвинуты вправо — в младшие 8 бит, в диапазон от 0 до 255.

  4. Дмитрий:

    Попробовал сделать таким образом.

  5. fapah13:

    Благодарствую за предоставленный самоучитель.

    Ради эксперимента написал код, задача которого принимать, хранить, и выводить значения сопротивлений стихиям наших баранов, то есть монстров, к холоду, огню, яду, молниям, с помощью битоых операций и флагов, по аналогии с RGBA. Берем монстра, присваиваем ему какие то резисты, конвертируем в 32битный RGBA формат, и накладывая маску выводим на экран. Как бы экономя при этом память. Я использовал всего 2 монстра, и экономии тут естественно нет, да и переменных получилось не мало. Но пойдет, как наглядный пример. Столько комментариев оставил больше для себя.

    И за это время образовались очевидные вопросы. Что если к этим 4-м атрибутам потребуется добавить еще парочку? Видимо сужать диапазон отведенный под каждый атрибут, или переходить на 64бита. А если их будет еще больше? А если придет нужда дать им возможность быть отрицательными(например -300 сопротивления холоду у африканского монстра)? Стало быть такой способ хранения может станет неприемлемым? Есть ли вообще такая практика или всё приходит к отдельным переменным?

    1. Ruslan:

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

  6. Артурка:

    Написал класс BitFlagsArticle используя код из теста:

    BitFlagsArticle.h:

    BitFlagsArticle.cpp:

    Source.cpp:

  7. Хей Лонг:

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

    1. Илья:

      На компилятор никогда надеяться нельзя. Вот представь: пишешь ты карточную игру и у тебя есть настройки количества игроков (2,3,4,5,6) и выбрать можно произвольное количество (хоть ни одного, хоть все). Можно, конечно, всюду bool пихнуть, но потом с таким интерфейсом крайне неприятно работать и в итоге появится ещё один слой абстракции, лишь бы упростить такую работу. Битовые флаги прекрасно решат проблему.

    2. Сергей:

      Интересная мысль. Да в С++ можно сделать поля в 1 бит.

      Но как инициализировать несколько полей сразу?

      С флагами это можно сделать просто c | f.

      Со структурой придётся так:

      К сожалению С++ так не умеет (или я не прав?):

  8. Константин:

    С Наступившим 2020! Встал (повис:-) вопрос: значения RGBA изменяются строго бит за битом, например 0000'0000 -> 0000'0001 -> 0000'0011 -> 0000'0111 -> 0000'0011 -> и т. д… т. е. как загораются светодиоды на электронном индикаторе уровня записи? Или в произвольном порядке: 0000'0000 -> 0000'0100 -> 1000'0111 -> 0101'0110 -> и т. д.?

    Код ниже превращает монитор в светофор:

    файл с заголовком ToolFor.h

    файл zvit.cpp с функцией main:

    1. Константин:

      Уррра! Допетрил сам — код в файле zvit.cpp должен быть такой:

  9. Александр:

    Что-то не понимаю в чём преимущества битового флага. Мы же ещё 8 переменных типа char определяем?

    1. Александр:

      Выше было написано в чём преимущество, оно возникает в том случае, когда переменных приблизительно 100 и более, в таком случае нам понадобится 100 флагов и 8 характеристик этих флагов(включение и исключение), без использования флагов было бы 800 переменных

  10. Кекс:

    Можно ли вместо включения бита сразу пользоваться переключателем ^=?

  11. Анастасия:

    Эта тема сначала показалась жутко сложной, но как только пошла практика, меня осенило.
    Иногда даже удивляешься, как люди умеют использовать имеющиеся ресурсы с такой гениальной экономией.

    Вот кстати практика. И конечно нужно было всё собрать в кучу 🙂

  12. Анастасия:

    Сначала мозг ушёл в бунт после строчки

    но потом всё стало интуитивно понятно. Кроме момента, когда просим пользователя ввести значения пикселя в 16-ричной системе исчисления. Я тут одна не понимаю, причём тут 16-ричная система исчисления и как пользователь (например, я) может осознанно что-то в ней вводить, предварительно не прикинув всё в десятеричной или хотя бы двоичной системе?

    1. Анастасия:

      Перечитала 36 урок и поняла, почему 0х10 = 0001 0000 и так далее.
      Суффикс 0х означает, что запись дальше — в 16-ричной системе.

      1. Денис:

        а я так и не понял, почему 0х10 = 0001 0000, расскажите плз)

        1. Артемий:

          36 урок

        2. Fray:

          Потому что одна шестнадцатеричная (дальше hex) цифра занимает 4 бита, с соответсвующим ей значением, например для цифры 5 из hex соответствует значение 0101, цифре 8 из hex — 1000. Если писать их вместе 0x58, то получится 0101 1000.

  13. Вячеслав:

    Попробовал все объединить и вот что вышло :

    1. Анастасия:

      Здравствуйте. У меня вопросы и замечания к Вашему коду (не просто так ведь Вы его на всеобщее обозрение выложили?):
      1) зачем

      ?
      2)

      насколько я понимаю, после условия if если хотим, чтобы действие выполнялось только при этом условии, точки с запятой быть не должно, иначе это два разных "стэйтмента".
      И второе. Вы, видимо, предполагаете, что если есть нужная единица для option_deleted, то всё выражение не будет 0, то есть будет истинным. Это вроде и понятно, но дойти до этой логической цепочки можно не сразу. Я сделала так:

      16 — это 0001 0000, у него есть одна из двух нужных единиц для deleted. Может, можно и лучше сделать. На мой взгляд тут есть сложный момент. Т.к. удалённая статья в двоичной системе должна иметь вид *1*1 ****
      3) "удалИна", хоть и закомментировано и к коду не относится, но всё равно режет глаза.

      1. Анастасия:

        Я была не права, переводя 0х80 в двоичную систему, как из десятичной с результатом 0101 0000. На самом деле это запись в шестнадцатеричной системе, а значит 1000 0000 и действительно можно написать

        так как условие после if будет true только если myArticleFlags будет иметь вид 1*** ****

        1. Victor:

          Здравствуйте! =) Как у вас успехи с C++ ? Вы его все ещё изучаете или уже — всё? =)
          Если ещё да — можно задать несколько вопросов??

      2. Анастасия:

        ответ Виктору: да, ещё изучаю, сейчас на 120-м уроке. Странно, что Вы хотите задать вопрос именно мне, но если так, то задавайте.

        1. Victor:

          Ничего странного. Элементарный "недорандом"… ))
          Вопрос тоже "странный". Не хотите ли в компании изучать ++?
          Прочитал тут одну книженцию об обучении, там весьма убедительно рекомендуют делать это в паре, или не большой группе, по огромному ряду причин.

        2. Анастасия:

          ответ Виктору: почему бы и нет? Я прерывалась на три недели, сейчас на 160 уроке. Давайте свяжемся как-то иначе и обсудим, что мы можем предпринять для повышения эффективности обучения. Моя почта amolchkova@mail.ru и скайп anastasiya.molchkova

        3. Victor:

          Написал и туда, и туда, на всякий случай ещё и сюда пишу. Вот
          email: cosintup@gmail.com

  14. Евгений:

    А не проще использовать битовые поля?

    Так вроде ещё удобней, вообще не надо никаких битовых операций)

    1. Виталий:

      битовые поля это хорошо, но побитовые операции несут за собой много интересных преимуществ
      читать нужно внимательнее

  15. korvell:

    Сложная тема, понял не до конца. 🙁

  16. Илья:

    да ладно, в хваленом с++ нельзя отформатировать литерал??? нули в начале не увеличивают память, они нужны как заполнитель байта.
    может быть я непонятно объясняю:
    х = 0х00abcdef //hex
    в двоичном коде:
    вывод по умолчанию (грубо 3 байта)
    1010 1011 1100 1101 1110 1111
    однако резерв 4 байта, куда делись нули в начале?
    0 0 a b c d e f
    0000 0000 1010 1011 1100 1101 1110 1111
    понятно что они ничего не значат, они нужны для правильного расчёта функций

    1. Фото аватара Юрий:

      Виноват. Неправильно понял ваш вопрос. Нужно заполнить вручную те 4 пустые биты с помощью побитовых операторов.

      1. Илья:

        в таком случае нужно еще проверять каждое значение функции, а это допольная операция, мне хочется найти решение чтобы значение переменной при всех раскладах было бы в формате 32 бита
        вывод в hex или bin неважно, нули на своем месте

        1. Фото аватара Юрий:

          Здесь уже помочь не смогу, чтобы значение переменной при всех раскладах было бы в формате 32 бита — значит нужно всё равно заполнить полностью эти 32 бита вручную — моё видение проблемы. Стандартное решение (и есть ли оно) мне неизвестно, попробуйте спросить на Stackoverflow.

  17. Илья:

    добрый день
    как указать в с++ чтобы переменная unsigned long int была в любом случае 32 бита, даже если она полностью состоит из 0.
    например в hex формате число 0x06ca6351 зайдёт в переменную как 6са6351

    1. Denis:

      А почему вы взяли , что так вообще можно.Коль уж каждой переменой типа(int) выделяется своя память , то она или выделяется или нет , так как откуда компьютер знает , не запихнете ли вы в него число побольше 0 , например даже 21902.Он динамически в работе программы в статические переменые просто бы не выделял новой памяти.P.S мой ответ нет , нельзя.

Добавить комментарий

Ваш E-mail не будет опубликован. Обязательные поля помечены *