На цьому уроці ми розглянемо бітові флаги і бітові маски в мові С++.
Примітка: Для деяких цей матеріал може здатися складним. Якщо ви застрягли або щось не зрозуміло — пропустіть цей урок (і наступний), в майбутньому ви зможете повернутися і розібратися детальніше. Цей урок не настільки важливий для прогресу у вивченні мови C++, як інші уроки, і викладений тут, в більшій мірі, для загального розвитку.
Бітові флаги
Використовуючи цілий байт для зберігання значення логічного типу даних, ви займаєте тільки 1 біт, а решта 7 з 8 — не використовуються. Хоча в цілому це нормально, але в особливих, ресурсоємних випадках, пов’язаних з безліччю логічних значень, може бути корисно “упакувати” 8 значень типу bool в 1 байт, заощадивши при цьому пам’ять і збільшивши, таким чином, продуктивність. Ці окремі біти і називаються бітовими флагами. Оскільки прямого доступу до цих біт немає, то для операцій з ними використовуються побітові оператори.
Примітка: На цьому уроці ми будемо використовувати значення з шістнадцяткової системи числення.
Наприклад:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Визначаємо 8 окремих бітових флаги (вони можуть представляти все, що ви захочете). // Зверніть увагу, що в C++11 краще використовувати "uint8_t" замість "unsigned char" const unsigned char option1 = 0x01; // шістнадцятковий літерал для 0000 0001 const unsigned char option2 = 0x02; // шістнадцятковий літерал для 0000 0010 const unsigned char option3 = 0x04; // шістнадцятковий літерал для 0000 0100 const unsigned char option4 = 0x08; // шістнадцятковий літерал для 0000 1000 const unsigned char option5 = 0x10; // шістнадцятковий літерал для 0001 0000 const unsigned char option6 = 0x20; // шістнадцятковий літерал для 0010 0000 const unsigned char option7 = 0x40; // шістнадцятковий літерал для 0100 0000 const unsigned char option8 = 0x80; // шістнадцятковий літерал для 1000 0000 // Байтове значення для зберігання комбінацій з 8 можливих варіантів unsigned char myflags = 0; // всі флаги/параметри вимкнені до старту |
Щоб дізнатися бітовий стан, використовуйте побітове І:
|
1 |
if (myflags & option4) ... // якщо встановлено option4, то робимо що-небудь |
Щоб увімкнути біти, використовуйте побітове АБО:
|
1 2 |
myflags |= option4; // вмикаємо option4 myflags |= (option4 | option5); // вмикаємо option4 і option5 |
Щоб вимкнути біти, використовуйте побітове І (в зворотній послідовності):
|
1 2 |
myflags &= ~option4; // вимикаємо option4 myflags &= ~(option4 | option5); // вимикаємо option4 і option5 |
Для перемикання між станами бітів, використовуйте побітове виключне АБО (XOR):
|
1 2 |
myflags ^= option4; // вмикаємо або вимикаємо option4 myflags ^= (option4 | option5); // змінюємо стан option4 і option5 |
Як приклад, візьмемо бібліотеку 3D-графіки OpenGL, в якій деякі функції приймають один або декілька бітових флагів в якості параметрів:
|
1 |
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // очищаємо буфер кольору і глибини |
GL_COLOR_BUFFER_BIT і GL_DEPTH_BUFFER_BIT визначаються наступним чином (в gl2.h):
|
1 2 3 |
#define GL_DEPTH_BUFFER_BIT 0x00000100 #define GL_STENCIL_BUFFER_BIT 0x00000400 #define GL_COLOR_BUFFER_BIT 0x00004000 |
Ось невеликий приклад:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <iostream> int main() { // Визначаємо набір з фізичних/емоційний станів const unsigned char isHungry = 0x01; // шістнадцятковий літерал для 0000 0001 const unsigned char isSad = 0x02; // шістнадцятковий літерал для 0000 0010 const unsigned char isMad = 0x04; // шістнадцятковий літерал для 0000 0100 const unsigned char isHappy = 0x08; // шістнадцятковий літерал для 0000 1000 const unsigned char isLaughing = 0x10; // шістнадцятковий літерал для 0001 0000 const unsigned char isAsleep = 0x20; // шістнадцятковий літерал для 0010 0000 const unsigned char isDead = 0x40; // шістнадцятковий літерал для 0100 0000 const unsigned char isCrying = 0x80; // шістнадцятковий літерал для 1000 0000 unsigned char me = 0; // всі флаги/параметри вимкнені до старту me |= isHappy | isLaughing; // я isHappy і isLaughing me &= ~isLaughing; // я вже не isLaughing // Запитуємо відразу декілька станів (ми будемо використовувати static_cast<bool> для конвертації результатів в значення типу bool) std::cout << "I am happy? " << static_cast<bool>(me & isHappy) << '\n'; std::cout << "I am laughing? " << static_cast<bool>(me & isLaughing) << '\n'; return 0; } |
Чому бітові флаги корисні?
Уважні читачі помітять, що в прикладах з 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 2 3 4 5 6 7 8 |
const unsigned char resistsPoison = 0x01; const unsigned char resistsLightning = 0x02; const unsigned char resistsFire = 0x04; const unsigned char resistsCold = 0x08; const unsigned char resistsTheft = 0x10; const unsigned char resistsAcid = 0x20; const unsigned char resistsParalysis = 0x40; const unsigned char resistsBlindness = 0x80; |
Нам знадобиться тільки 1 байт для зберігання опору кожного монстра і одноразова плата в 8 байт для типів атак.
Таким чином, нам знадобиться тільки 108 байт або приблизно в 8 разів менше пам’яті.
У більшості програм, збережений обсяг пам’яті, з використанням бітових флагів, не вартий доданої складності. Але в програмах, де є десятки тисяч або навіть мільйони схожих об’єктів, їх використання може значно заощадити пам’ять. Погодьтеся, знати про такий корисний трюк не завадить.
Випадок №2: Уявіть, що у вас є функція, яка може приймати будь-яку комбінацію з 32 різних варіантів. Одним із способів написання такої функції є використання 32 окремих логічних параметрів:
|
1 |
void someFunction(bool option1, bool option2, bool option3, bool option4, bool option5, bool option6, bool option7, bool option8, bool option9, bool option10, bool option11, bool option12, bool option13, bool option14, bool option15, bool option16, bool option17, bool option18, bool option19, bool option20, bool option21, bool option22, bool option23, bool option24, bool option25, bool option26, bool option27, bool option28, bool option29, bool option30, bool option31, bool option32); |
Потім, якщо ви захочете викликати функцію з 10-м і 32-м параметрами, встановленими як true — вам доведеться зробити щось типу наступного:
|
1 |
someFunction(false, false, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, true); |
Тобто порахувати всі варіанти як false, крім 10 і 32 — вони true. Читати такий код складно, та й потрібно пам’ятати порядкові номери потрібних параметрів (10 і 32 чи 11 і 33?). Такий код не може бути ефективним.
А ось якщо визначати функцію, використовуючи бітові флаги:
|
1 |
void someFunction(unsigned int options); |
То можна вибирати і передавати тільки потрібні параметри:
|
1 |
someFunction(option10 | option32); |
Крім того, що це читабельніше, це також ефективніше і продуктивніше, оскільки включає тільки 2 операції (одне побітове АБО і одна передача параметрів).
Ось чому в OpenGL використовуються бітові флаги замість довгої послідовності логічних значень.
Також, якщо у вас є невикористовувані бітові флаги і вам потрібно додати параметри пізніше, ви можете просто визначити бітовий флаг. Немає необхідності в зміні прототипу функції, а це плюс до забезпечення зворотної сумісності.
Введення в std::bitset
Всі ці біти, бітові флаги, операції-маніпуляції — це все втомлює, чи не так? На щастя, в Стандартній бібліотеці C++ є такий об’єкт, як std::bitset, який спрощує роботу з бітовими флагами.
Для його використання необхідно підключити заголовковий файл bitset, а потім визначити змінну типу std::bitset, вказавши необхідну кількість біт. Вона повинна бути константою часу компіляції.
|
1 2 3 |
#include <bitset> std::bitset<8> bits; // нам потрібно 8 біт |
При бажанні std::bitset можна ініціалізувати початковим набором значень:
|
1 2 3 4 |
#include <bitset> std::bitset<8> bits(option1 | option2) ; // почнемо з увімкнених option1 і option2 std::bitset<8> morebits(0x2) ; // почнемо з бітового шаблону 0000 0010 |
Зверніть увагу, наше початкове значення конвертується в двійкову систему. Оскільки ми ввели шістнадцяткове 2, то std::bitset перетворює його в двійкове 0000 0010.
У std::bitset є 4 основні функції:
функція test() — дозволяє дізнатися значення біта (0 чи 1).
функція set() — дозволяє увімкнути біти (якщо вони вже увімкнені, то нічого не відбудеться).
функція reset() — дозволяє вимкнути біти (якщо вони вже вимкнені, то нічого не відбудеться).
функція flip() — дозволяє змінити значення біт на протилежні (з 0 на 1 або з 1 на 0).
Кожна з цих функцій приймає в якості параметрів позиції біт. Позиція крайнього правого біта (останнього) — 0, потім порядковий номер зростає з кожним наступним бітом вліво (1, 2, 3, 4 і т.д.). Намагайтеся давати змістовні імена бітовим індексам (або шляхом присвоювання їх константним змінним, або за допомогою перерахувань — про них ми поговоримо пізніше).
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#include <iostream> #include <bitset> // Зверніть увагу, використовуючи std::bitset, наші options відповідають порядковим номерам біт, а не їх значенням const int option_1 = 0; const int option_2 = 1; const int option_3 = 2; const int option_4 = 3; const int option_5 = 4; const int option_6 = 5; const int option_7 = 6; const int option_8 = 7; int main() { // Пам'ятайте, що відлік біт починається не з 1, а з 0 std::bitset<8> bits(0x2); // нам потрібно 8 біт, тому почнемо з бітового шаблону 0000 0010 bits.set(option_5); // вмикаємо 4-й біт - його значення зміниться на 1 (тепер ми маємо 0001 0010) bits.flip(option_6); // змінюємо значення 5-го біта на протилежне (тепер ми маємо 0011 0010) bits.reset(option_6); // вимикаємо 5-й біт - його значення знову 0 (тепер ми маємо 0001 0010) std::cout << "Bit 4 has value: " << bits.test(option_5) << '\n'; std::cout << "Bit 5 has value: " << bits.test(option_6) << '\n'; std::cout << "All the bits: " << bits << '\n'; return 0; } |
Результат виконання програми:
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 біти, значення яких і виводимо в консоль:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <iostream> int main() { const unsigned int lowMask = 0xF; // бітова маска для зберігання останніх 4 біт (шістнадцятковий літерал для 0000 0000 0000 1111) std::cout << "Enter an integer: "; int num; std::cin >> num; num &= lowMask; // видаляємо перші біти, щоб залишити останні std::cout << "The 4 low bits have value: " << num << '\n'; return 0; } |
Результат виконання програми:
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:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#include <iostream> int main() { const unsigned int redBits = 0xFF000000; const unsigned int greenBits = 0x00FF0000; const unsigned int blueBits = 0x0000FF00; const unsigned int alphaBits = 0x000000FF; std::cout << "Enter a 32-bit RGBA color value in hexadecimal (e.g. FF7F3300): "; unsigned int pixel; std::cin >> std::hex >> pixel; // std::hex дозволяє вводити шістнадцяткові значення // Використовуємо побітове І для ізоляції червоних пікселів, а потім зміщуємо значення вправо в діапазон 0-255 unsigned char red = (pixel & redBits) >> 24; unsigned char green = (pixel & greenBits) >> 16; unsigned char blue = (pixel & blueBits) >> 8; unsigned char alpha = pixel & alphaBits; std::cout << "Your color contains:\n"; std::cout << static_cast<int>(red) << " of 255 red\n"; std::cout << static_cast<int>(green) << " of 255 green\n"; std::cout << static_cast<int>(blue) << " of 255 blue\n"; std::cout << static_cast<int>(alpha) << " of 255 alpha\n"; return 0; } |
Результат виконання програми:
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. У такому випадку головним байтом є альфа.
Висновки
Давайте коротко повторимо те, як вмикати, вимикати, перемикати і робити запити бітових флагів.
Для запиту бітового стану використовується побітове І:
|
1 |
if (myflags & option4) ... // якщо встановлений option4, то робимо що-небудь |
Для увімкнення біт використовується побітове АБО:
|
1 2 |
myflags |= option4; // вмикаємо option4 myflags |= (option4 | option5); // вмикаємо option4 і option5 |
Для вимкнення біт використовується побітове І у зворотній комбінації:
|
1 2 |
myflags &= ~option4; // вимикаємо option4 myflags &= ~(option4 | option5); // вимикаємо option4 і option5 |
Для перемикання між бітовими станами використовується побітове виключне АБО (XOR):
|
1 2 |
myflags ^= option4; // вмикаємо або вимикаємо option4 myflags ^= (option4 | option5); // змінюємо на протилежні option4 і option5 |
Тест
Є наступний фрагмент коду:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
int main() { unsigned char option_viewed = 0x01; unsigned char option_edited = 0x02; unsigned char option_favorited = 0x04; unsigned char option_shared = 0x08; unsigned char option_deleted = 0x80; unsigned char myArticleFlags; return 0; } |
Примітка: Стаття — це myArticleFlags.
Завдання №1
Додайте рядок коду, щоб позначити статтю як вже прочитану (option_viewed).
Завдання №2
Додайте рядок коду, щоб перевірити, чи була стаття видалена (option_deleted).
Завдання №3
Додайте рядок коду, щоб відкріпити статтю від закріпленого місця (option_favorited).
Завдання №4
Чому наступні два рядки ідентичні?
|
1 2 |
myflags &= ~(option4 | option5); // вимикаємо option4 і option5 myflags &= ~option4 & ~option5; // вимикаємо option4 і option5 |
Відповіді
Відповідь №1
myArticleFlags |= option_viewed;
Відповідь №2
if (myArticleFlags & option_deleted) …
Відповідь №3
myArticleFlags &= ~option_favorited;
Відповідь №4
Правила де Моргана повідомляють, що якщо ми використовуємо побітове НЕ, то оператори І та АБО міняються місцями. Тому ~(option4 | option5) стає ~option4 & ~option5.
