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

   ⁄ 

 Обновлено 9 Мар 2017

  ⁄   

Примечание: Этот материал немного сложный. Если Вы застряли или что-то непонятно — спокойно пропускайте урок, позднее вернетесь к нему и разберетесь детальнее.

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

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

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

Например:

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

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

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

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

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

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

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

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

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

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

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?). И не эффективно это, так как при каждом вызове функции указываются все 32 варианта.

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

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

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

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

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

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

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

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

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

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

В 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 — выводится значение всех битов в bitset.

Помните, инициализируемое значение bitset рассматривается как двоичное, в то время как такие функции 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

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

Используется еще 4-ое значение, которое называется А. «А» означает «альфа» и отвечает за прозрачность. Если А = 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 (6 оценок, среднее: 4,83 из 5)
Загрузка...
Поделиться в:
Подписаться на обновления:

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

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