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

  Юрій  | 

  Оновл. 4 Вер 2021  | 

 437

На цьому уроці ми розглянемо бітові флаги і бітові маски в мові С++.

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

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

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