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

  Юрий  | 

    | 

  Обновл. 15 Апр 2019  | 

 20860

 ǀ   9 

Примечание: Для некоторых этот материал может показаться немного сложным. Если вы застряли или что-то не понятно — пропустите этот урок, в будущем сможете вернуться и разобраться детальнее. Он не столь важен для прогресса в изучении 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 для битового флага)! Так зачем же тогда нужны битовые флаги?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Вот почему в 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

Цветные дисплейные устройства, такие как телевизоры и мониторы, состоят из миллионов пикселей, каждый из которых может отображать точку цвета. Точка цвета состоит из трёх пучков: один красный, один зелёный и один синий («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 (51 оценок, среднее: 4,78 из 5)
Загрузка...

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

  1. Аватар Вячеслав:

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

  2. Аватар Евгений:

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

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

  3. Аватар korvell:

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

  4. Аватар Илья:

    да ладно, в хваленом с++ нельзя отформатировать литерал??? нули в начале не увеличивают память, они нужны как заполнитель байта.
    может быть я непонятно объясняю:
    х = 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.

  5. Аватар Илья:

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

    1. Аватар Denis:

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

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

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