Урок №49. Бітові прапори та бітові маски

  Юрій  | 

  Оновл. 10 Тра 2020  | 

 81

Примітка: Для деяких цей матеріал може здатися трохи складним. Якщо ви застрягли або щось не зрозуміло — пропустіть цей урок, в майбутньому зможете повернутися і розібратися детальніше. Він не настільки важливий для прогресу у вивченні 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 для бітового прапора)! Так навіщо ж тоді потрібні бітові прапори?

Вони використовуються в двох випадках:

Випадок №1: Якщо у вас багато ідентичних бітових прапорів.

Замість однієї змінної myflags, розглянемо випадок, коли у вас є дві змінні: myflags1 і myflags2, кожна з яких може зберігати 8 значень. Якщо ви визначите їх як два окремих логічних набори, то вам потрібно буде 16 логічних значень і, таким чином, 16 байт. Однак, з використанням бітових прапорів, вам потрібно буде тільки 10 байт (8 для визначення параметрів і 1 для кожної змінної). А ось якщо у вас буде 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 операції (одне побітове АБО і одна передача параметрів).

Ось чому в OpenGL використовують бітові прапори замість довгої послідовності логічних значень.

Також, якщо у вас є невикористовувані бітові прапори і вам потрібно додати параметри пізніше, ви можете просто визначити бітовий прапор. Немає необхідності в зміні прототипу функції, а це плюс до забезпечення зворотної сумісності.

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

Всі ці біти, бітові прапори, операції-маніпуляції — це все втомлює, чи не так? На щастя, в Стандартній бібліотеці C++ є такий об’єкт, як std::bitset, який спрощує роботу з бітовими прапорами.

Для його використання необхідно підключити заголовковий файл <bitset>, а потім визначити змінну типу std::bitset, вказавши необхідну кількість біт. Вона повинна бути константою часу компіляції.

При бажанні 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 Зірок (1 оцінок, середня: 5,00 з 5)
Loading...

Залишити відповідь

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