W poprzedniej lekcji na temat operatorów bitowych (O.2 -- Operatory bitowe) omówiliśmy, w jaki sposób różne operatory bitowe stosują operatory logiczne do każdego bitu w operandach. Teraz, gdy rozumiemy, jak działają, przyjrzyjmy się, jak są częściej używane.
Maski bitowe
Aby manipulować pojedynczymi bitami (np. włączać je lub wyłączać), potrzebujemy sposobu na zidentyfikowanie konkretnych bitów, którymi chcemy manipulować. Niestety operatory bitowe nie wiedzą, jak pracować z pozycjami bitowymi. Zamiast tego działają z maskami bitowymi.
A maska bitowa to predefiniowany zestaw bitów używany do wyboru, które konkretne bity będą modyfikowane w kolejnych operacjach.
Rozważmy rzeczywisty przypadek, w którym chcesz pomalować ramę okna. Jeśli nie będziesz ostrożny, ryzykujesz pomalowaniem nie tylko ramy okna, ale także samego szkła. Możesz kupić taśmę maskującą i przykleić ją do szyby oraz innych części, których nie chcesz malować. Następnie podczas malowania taśma maskująca uniemożliwia dotarcie farby do wszystkiego, czego nie chcesz malować. Na koniec malowane są tylko niezamaskowane części (te, które chcesz pomalować).
Maska bitowa zasadniczo spełnia tę samą funkcję w przypadku bitów — maska bitowa blokuje operatorom bitowym dotykanie bitów, których nie chcemy modyfikować, i umożliwia dostęp do tych, które chcemy zmodyfikować.
Najpierw przyjrzyjmy się, jak zdefiniować proste maski bitowe, a następnie pokażemy, jak z nich korzystać je.
Definiowanie masek bitowych w C++14
Najprostszym zestawem masek bitowych jest zdefiniowanie jednej maski bitowej dla każdej pozycji bitowej. Używamy zer do maskowania bitów, na których nam nie zależy, a jedynek do oznaczenia bitów, które chcemy zmodyfikować.
Chociaż maski bitowe mogą być literałami, często definiuje się je jako stałe symboliczne, dzięki czemu można im nadać znaczącą nazwę i łatwo je ponownie wykorzystać.
Ponieważ C++ 14 obsługuje literały binarne, zdefiniowanie tych masek bitowych jest łatwe:
#include <cstdint>
constexpr std::uint8_t mask0{ 0b0000'0001 }; // represents bit 0
constexpr std::uint8_t mask1{ 0b0000'0010 }; // represents bit 1
constexpr std::uint8_t mask2{ 0b0000'0100 }; // represents bit 2
constexpr std::uint8_t mask3{ 0b0000'1000 }; // represents bit 3
constexpr std::uint8_t mask4{ 0b0001'0000 }; // represents bit 4
constexpr std::uint8_t mask5{ 0b0010'0000 }; // represents bit 5
constexpr std::uint8_t mask6{ 0b0100'0000 }; // represents bit 6
constexpr std::uint8_t mask7{ 0b1000'0000 }; // represents bit 7Teraz mamy zbiór stałych symbolicznych reprezentujących każdą pozycję bitu. Możemy ich używać do manipulowania bitami (co za chwilę pokażemy, jak to zrobić).
Definiowanie masek bitowych w C++ 11 i wcześniejszych
Ponieważ C++11 nie obsługuje literałów binarnych, musimy użyć innych metod, aby ustawić stałe symboliczne. Można to zrobić na dwie dobre metody.
Pierwsza metoda polega na użyciu literałów szesnastkowych.
Powiązana treść
Mówimy o postaci szesnastkowej na lekcji 5.2 -- Literały.
Oto sposób konwersji wartości szesnastkowej na binarny:
| Szesnastkowy | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
| Binary | 0000 | 0001 | 0010 | 0011 | 0100 | 0101 | 0110 | 0111 | 1000 | 1001 | 1010 | 1011 | 1100 | 1101 | 1110 | 1111 |
Dlatego możemy zdefiniować maski bitowe za pomocą systemu szesnastkowego w następujący sposób:
constexpr std::uint8_t mask0{ 0x01 }; // hex for 0000 0001
constexpr std::uint8_t mask1{ 0x02 }; // hex for 0000 0010
constexpr std::uint8_t mask2{ 0x04 }; // hex for 0000 0100
constexpr std::uint8_t mask3{ 0x08 }; // hex for 0000 1000
constexpr std::uint8_t mask4{ 0x10 }; // hex for 0001 0000
constexpr std::uint8_t mask5{ 0x20 }; // hex for 0010 0000
constexpr std::uint8_t mask6{ 0x40 }; // hex for 0100 0000
constexpr std::uint8_t mask7{ 0x80 }; // hex for 1000 0000Czasami początkowe szesnastkowe zera zostaną pominięte (np. zamiast tego z 0x01 po prostu zobaczysz 0x1). Tak czy inaczej, może to być trochę trudne do odczytania, jeśli nie jesteś zaznajomiony z konwersją zapisu szesnastkowego na binarny.
Łatwiejszą metodą jest użycie operatora lewego przesunięcia do przesunięcia pojedynczego bitu w odpowiednie miejsce:
constexpr std::uint8_t mask0{ 1 << 0 }; // 0000 0001
constexpr std::uint8_t mask1{ 1 << 1 }; // 0000 0010
constexpr std::uint8_t mask2{ 1 << 2 }; // 0000 0100
constexpr std::uint8_t mask3{ 1 << 3 }; // 0000 1000
constexpr std::uint8_t mask4{ 1 << 4 }; // 0001 0000
constexpr std::uint8_t mask5{ 1 << 5 }; // 0010 0000
constexpr std::uint8_t mask6{ 1 << 6 }; // 0100 0000
constexpr std::uint8_t mask7{ 1 << 7 }; // 1000 0000Testowanie trochę (aby sprawdzić, czy jest włączone, czy wyłączone)
Teraz, gdy mamy zestaw masek bitowych, możemy ich używać w połączeniu ze zmienną flagi bitowej do manipulowania nasze flagi bitowe.
Aby określić, czy bit jest włączony, czy wyłączony, używamy bitowego AND w połączeniu z maską bitową dla odpowiedniego bitu:
#include <cstdint>
#include <iostream>
int main()
{
[[maybe_unused]] constexpr std::uint8_t mask0{ 0b0000'0001 }; // represents bit 0
[[maybe_unused]] constexpr std::uint8_t mask1{ 0b0000'0010 }; // represents bit 1
[[maybe_unused]] constexpr std::uint8_t mask2{ 0b0000'0100 }; // represents bit 2
[[maybe_unused]] constexpr std::uint8_t mask3{ 0b0000'1000 }; // represents bit 3
[[maybe_unused]] constexpr std::uint8_t mask4{ 0b0001'0000 }; // represents bit 4
[[maybe_unused]] constexpr std::uint8_t mask5{ 0b0010'0000 }; // represents bit 5
[[maybe_unused]] constexpr std::uint8_t mask6{ 0b0100'0000 }; // represents bit 6
[[maybe_unused]] constexpr std::uint8_t mask7{ 0b1000'0000 }; // represents bit 7
std::uint8_t flags{ 0b0000'0101 }; // 8 bits in size means room for 8 flags
std::cout << "bit 0 is " << (static_cast<bool>(flags & mask0) ? "on\n" : "off\n");
std::cout << "bit 1 is " << (static_cast<bool>(flags & mask1) ? "on\n" : "off\n");
return 0;
}Wypisuje:
bit 0 is on bit 1 is off
Zbadajmy, jak to działa.
W przypadku flags & mask0 mamy 0000'0101 & 0000'0001. Uporządkujmy to:
0000'0101 & 0000'0001 --------- 0000'0001
Następnie przeprowadzamy casting 0000'0001 do bool. Ponieważ każda liczba różna od zera jest konwertowana na true i ta wartość ma cyfrę różną od zera, daje to true.
W przypadku flags & mask1 mamy 0000'0101 & 0000'0010. Uporządkujmy to:
0000'0101 & 0000'0010 --------- 0000'0000
Ponieważ wartość zerowa jest konwertowana na false a wartość ta ma tylko cyfry zerowe, daje to false.
Ustawianie bitu
Aby ustawić (włączyć) bit (na wartość 1), używamy bitowego OR równego (operator |=) w połączeniu z maską bitową dla odpowiedniego bitu:
#include <cstdint>
#include <iostream>
int main()
{
[[maybe_unused]] constexpr std::uint8_t mask0{ 0b0000'0001 }; // represents bit 0
[[maybe_unused]] constexpr std::uint8_t mask1{ 0b0000'0010 }; // represents bit 1
[[maybe_unused]] constexpr std::uint8_t mask2{ 0b0000'0100 }; // represents bit 2
[[maybe_unused]] constexpr std::uint8_t mask3{ 0b0000'1000 }; // represents bit 3
[[maybe_unused]] constexpr std::uint8_t mask4{ 0b0001'0000 }; // represents bit 4
[[maybe_unused]] constexpr std::uint8_t mask5{ 0b0010'0000 }; // represents bit 5
[[maybe_unused]] constexpr std::uint8_t mask6{ 0b0100'0000 }; // represents bit 6
[[maybe_unused]] constexpr std::uint8_t mask7{ 0b1000'0000 }; // represents bit 7
std::uint8_t flags{ 0b0000'0101 }; // 8 bits in size means room for 8 flags
std::cout << "bit 1 is " << (static_cast<bool>(flags & mask1) ? "on\n" : "off\n");
flags |= mask1; // turn on bit 1
std::cout << "bit 1 is " << (static_cast<bool>(flags & mask1) ? "on\n" : "off\n");
return 0;
}Wypisuje:
bit 1 is off bit 1 is on
Możemy także włączyć wielokrotność bitów jednocześnie za pomocą Bitowo LUB:
flags |= (mask4 | mask5); // turn bits 4 and 5 on at the same timeZerowanie bitu
Aby zresetować (wyczyścić) bit (do wartości 0), używamy Bitowe AND i Bitowo NOT razem:
#include <cstdint>
#include <iostream>
int main()
{
[[maybe_unused]] constexpr std::uint8_t mask0{ 0b0000'0001 }; // represents bit 0
[[maybe_unused]] constexpr std::uint8_t mask1{ 0b0000'0010 }; // represents bit 1
[[maybe_unused]] constexpr std::uint8_t mask2{ 0b0000'0100 }; // represents bit 2
[[maybe_unused]] constexpr std::uint8_t mask3{ 0b0000'1000 }; // represents bit 3
[[maybe_unused]] constexpr std::uint8_t mask4{ 0b0001'0000 }; // represents bit 4
[[maybe_unused]] constexpr std::uint8_t mask5{ 0b0010'0000 }; // represents bit 5
[[maybe_unused]] constexpr std::uint8_t mask6{ 0b0100'0000 }; // represents bit 6
[[maybe_unused]] constexpr std::uint8_t mask7{ 0b1000'0000 }; // represents bit 7
std::uint8_t flags{ 0b0000'0101 }; // 8 bits in size means room for 8 flags
std::cout << "bit 2 is " << (static_cast<bool>(flags & mask2) ? "on\n" : "off\n");
flags &= ~mask2; // turn off bit 2
std::cout << "bit 2 is " << (static_cast<bool>(flags & mask2) ? "on\n" : "off\n");
return 0;
}Wypisuje:
bit 2 is on bit 2 is off
Możemy wyłączyć wiele bitów jednocześnie time:
flags &= ~(mask4 | mask5); // turn bits 4 and 5 off at the same timeKluczowa informacja
Niektóre kompilatory mogą narzekać na konwersję znaku za pomocą tej linii:
flags &= ~mask2;
Ponieważ typ mask2 jest mniejszy niż int, operator~ powoduje, że operand mask2 poddaje się promocji całkowej do typu int. Następnie kompilator narzeka, że próbujemy użyć operator&= gdzie lewy operand jest bez znaku, a prawy operand jest podpisany.
W takim przypadku spróbuj wykonać następujące czynności:
flags &= static_cast<std::uint8_t>(~mask2);Omawiamy ten problem na lekcji O.2 -- Operatory bitowe.
Trochę odwracamy
Aby przełączyć (odwróć) stan bitowy (od 0 do 1 lub od 1 do 0), używamy Bitowego XOR:
#include <cstdint>
#include <iostream>
int main()
{
[[maybe_unused]] constexpr std::uint8_t mask0{ 0b0000'0001 }; // represents bit 0
[[maybe_unused]] constexpr std::uint8_t mask1{ 0b0000'0010 }; // represents bit 1
[[maybe_unused]] constexpr std::uint8_t mask2{ 0b0000'0100 }; // represents bit 2
[[maybe_unused]] constexpr std::uint8_t mask3{ 0b0000'1000 }; // represents bit 3
[[maybe_unused]] constexpr std::uint8_t mask4{ 0b0001'0000 }; // represents bit 4
[[maybe_unused]] constexpr std::uint8_t mask5{ 0b0010'0000 }; // represents bit 5
[[maybe_unused]] constexpr std::uint8_t mask6{ 0b0100'0000 }; // represents bit 6
[[maybe_unused]] constexpr std::uint8_t mask7{ 0b1000'0000 }; // represents bit 7
std::uint8_t flags{ 0b0000'0101 }; // 8 bits in size means room for 8 flags
std::cout << "bit 2 is " << (static_cast<bool>(flags & mask2) ? "on\n" : "off\n");
flags ^= mask2; // flip bit 2
std::cout << "bit 2 is " << (static_cast<bool>(flags & mask2) ? "on\n" : "off\n");
flags ^= mask2; // flip bit 2
std::cout << "bit 2 is " << (static_cast<bool>(flags & mask2) ? "on\n" : "off\n");
return 0;
}Wypisuje:
bit 2 is on bit 2 is off bit 2 is on
Możemy odwracać wiele bitów jednocześnie:
flags ^= (mask4 | mask5); // flip bits 4 and 5 at the same timeMaski bitowe i std::bitset
std::bitset obsługują pełny zestaw operatorów bitowych. Mimo że łatwiej jest używać funkcji (testowanie, ustawianie, resetowanie i odwracanie) do modyfikowania pojedynczych bitów, jeśli chcesz, możesz używać operatorów bitowych i masek bitowych.
Dlaczego miałbyś tego chcieć? Funkcje pozwalają jedynie na modyfikację pojedynczych bitów. Operatory bitowe umożliwiają modyfikację wielu bitów na raz.
#include <bitset>
#include <iostream>
int main()
{
[[maybe_unused]] constexpr std::bitset<8> mask0{ 0b0000'0001 }; // represents bit 0
[[maybe_unused]] constexpr std::bitset<8> mask1{ 0b0000'0010 }; // represents bit 1
[[maybe_unused]] constexpr std::bitset<8> mask2{ 0b0000'0100 }; // represents bit 2
[[maybe_unused]] constexpr std::bitset<8> mask3{ 0b0000'1000 }; // represents bit 3
[[maybe_unused]] constexpr std::bitset<8> mask4{ 0b0001'0000 }; // represents bit 4
[[maybe_unused]] constexpr std::bitset<8> mask5{ 0b0010'0000 }; // represents bit 5
[[maybe_unused]] constexpr std::bitset<8> mask6{ 0b0100'0000 }; // represents bit 6
[[maybe_unused]] constexpr std::bitset<8> mask7{ 0b1000'0000 }; // represents bit 7
std::bitset<8> flags{ 0b0000'0101 }; // 8 bits in size means room for 8 flags
std::cout << "bit 1 is " << (flags.test(1) ? "on\n" : "off\n");
std::cout << "bit 2 is " << (flags.test(2) ? "on\n" : "off\n");
flags ^= (mask1 | mask2); // flip bits 1 and 2
std::cout << "bit 1 is " << (flags.test(1) ? "on\n" : "off\n");
std::cout << "bit 2 is " << (flags.test(2) ? "on\n" : "off\n");
flags |= (mask1 | mask2); // turn bits 1 and 2 on
std::cout << "bit 1 is " << (flags.test(1) ? "on\n" : "off\n");
std::cout << "bit 2 is " << (flags.test(2) ? "on\n" : "off\n");
flags &= ~(mask1 | mask2); // turn bits 1 and 2 off
std::cout << "bit 1 is " << (flags.test(1) ? "on\n" : "off\n");
std::cout << "bit 2 is " << (flags.test(2) ? "on\n" : "off\n");
return 0;
}Wypisuje:
bit 1 is off bit 2 is on bit 1 is on bit 2 is off bit 1 is on bit 2 is on bit 1 is off bit 2 is off
Nadanie maskom bitowym znaczenia
Nazywanie naszych masek bitowych „maska1” lub „maska2” informuje nas, którym bitem manipulujemy, ale nie daje nam żadnych wskazówek, do czego właściwie ta flaga bitowa jest używana.
Najlepszą praktyką jest nadawanie maskom bitowym przydatnych nazw w celu udokumentowania znaczenia bitu flagi. Oto przykład z gry, którą moglibyśmy napisać:
#include <cstdint>
#include <iostream>
int main()
{
// Define a bunch of physical/emotional states
[[maybe_unused]] constexpr std::uint8_t isHungry { 1 << 0 }; // 0000 0001
[[maybe_unused]] constexpr std::uint8_t isSad { 1 << 1 }; // 0000 0010
[[maybe_unused]] constexpr std::uint8_t isMad { 1 << 2 }; // 0000 0100
[[maybe_unused]] constexpr std::uint8_t isHappy { 1 << 3 }; // 0000 1000
[[maybe_unused]] constexpr std::uint8_t isLaughing { 1 << 4 }; // 0001 0000
[[maybe_unused]] constexpr std::uint8_t isAsleep { 1 << 5 }; // 0010 0000
[[maybe_unused]] constexpr std::uint8_t isDead { 1 << 6 }; // 0100 0000
[[maybe_unused]] constexpr std::uint8_t isCrying { 1 << 7 }; // 1000 0000
std::uint8_t me{}; // all flags/options turned off to start
me |= (isHappy | isLaughing); // I am happy and laughing
me &= ~isLaughing; // I am no longer laughing
// Query a few states
// (we'll use static_cast<bool> to interpret the results as a boolean value)
std::cout << std::boolalpha; // print true or false instead of 1 or 0
std::cout << "I am happy? " << static_cast<bool>(me & isHappy) << '\n';
std::cout << "I am laughing? " << static_cast<bool>(me & isLaughing) << '\n';
return 0;
}Oto ten sam przykład zaimplementowany przy użyciu std::bitset:
#include <bitset>
#include <iostream>
int main()
{
// Define a bunch of physical/emotional states
[[maybe_unused]] constexpr std::bitset<8> isHungry { 0b0000'0001 };
[[maybe_unused]] constexpr std::bitset<8> isSad { 0b0000'0010 };
[[maybe_unused]] constexpr std::bitset<8> isMad { 0b0000'0100 };
[[maybe_unused]] constexpr std::bitset<8> isHappy { 0b0000'1000 };
[[maybe_unused]] constexpr std::bitset<8> isLaughing { 0b0001'0000 };
[[maybe_unused]] constexpr std::bitset<8> isAsleep { 0b0010'0000 };
[[maybe_unused]] constexpr std::bitset<8> isDead { 0b0100'0000 };
[[maybe_unused]] constexpr std::bitset<8> isCrying { 0b1000'0000 };
std::bitset<8> me{}; // all flags/options turned off to start
me |= (isHappy | isLaughing); // I am happy and laughing
me &= ~isLaughing; // I am no longer laughing
// Query a few states (we use the any() function to see if any bits remain set)
std::cout << std::boolalpha; // print true or false instead of 1 or 0
std::cout << "I am happy? " << (me & isHappy).any() << '\n';
std::cout << "I am laughing? " << (me & isLaughing).any() << '\n';
return 0;
}Tutaj dwie uwagi: Po pierwsze, std::bitset nie ma ładnej funkcji, która pozwala na odpytywanie bitów przy użyciu maski bitowej. Jeśli więc chcesz używać masek bitowych zamiast indeksów pozycyjnych, będziesz musiał użyć Bitowe AND , aby zapytać o bity. Po drugie, korzystamy z funkcji any(), która zwraca wartość true, jeśli ustawione są jakieś bity, lub false w przeciwnym razie, aby sprawdzić, czy bit, o który pytaliśmy, pozostaje włączony, czy wyłączony.
Kiedy flagi bitowe są najbardziej przydatne?
Wnikliwi czytelnicy mogą zauważyć, że powyższe przykłady w rzeczywistości nie oszczędzają pamięci. 8 oddzielnych wartości logicznych zwykle zajmuje 8 bajtów. Ale powyższe przykłady (przy użyciu std::uint8_t) używają 9 bajtów -- 8 bajtów do zdefiniowania masek bitowych i 1 bajt na zmienną flagi!
Flagi bitowe mają największy sens, gdy masz wiele identycznych zmiennych flag. Na przykład w powyższym przykładzie wyobraź sobie, że zamiast jednej osoby (mnie) masz ich 100. Jeśli użyłbyś 8 wartości logicznych na osobę (po jednej na każdy możliwy stan), użyłbyś 800 bajtów pamięci. W przypadku flag bitowych należy użyć 8 bajtów na maski bitowe i 100 bajtów na zmienne flag bitowych, co daje w sumie 108 bajtów pamięci — około 8 razy mniej pamięci.
W przypadku większości programów ilość pamięci zapisanej przy użyciu flag bitowych nie jest warta dodatkowej złożoności. Jednak w programach, w których istnieją dziesiątki tysięcy, a nawet miliony podobnych obiektów, użycie flag bitowych może znacznie zmniejszyć zużycie pamięci. Jeśli jej potrzebujesz, warto mieć tę optymalizację w swoim zestawie narzędzi.
Istnieje inny przypadek, w którym flagi bitowe i maski bitowe mogą mieć sens. Wyobraź sobie, że masz funkcję, która może przyjąć dowolną kombinację 32 różnych opcji. Jednym ze sposobów napisania tej funkcji byłoby użycie 32 indywidualnych parametrów logicznych:
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);Mam nadzieję, że nadasz swoim parametrom bardziej opisowe nazwy, ale chodzi o pokazanie, jak okropnie długa jest lista parametrów.
Wtedy, gdy chciałbyś wywołać funkcję z opcjami 10 i 32 ustawionymi na true, musiałbyś to zrobić w ten sposób:
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);To jest absurdalnie trudne do odczytania (czy opcja 9, 10 czy 11 jest ustawiona na true?), a także oznacza, że musisz pamiętać, który argument odpowiada której opcji (czy ustawianie „flagi edycji” to 9., 10. czy 11. parametr?).
Zamiast tego, jeśli zdefiniowałeś funkcję przy użyciu flag bitowych w ten sposób:
void someFunction(std::bitset<32> options);Wtedy możesz użyć flag bitowych do przekazania tylko tych opcji, które chcesz chciałem:
someFunction(option10 | option32);Jest to znacznie bardziej czytelne.
Jest to jeden z powodów, dla których OpenGL, dobrze ceniona biblioteka grafiki 3D, zdecydowała się na użycie parametrów flag bitowych zamiast wielu kolejnych parametrów logicznych.
Oto przykładowe wywołanie funkcji z OpenGL:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // clear the color and the depth bufferGL_COLOR_BUFFER_BIT i GL_DEPTH_BUFFER_BIT to maski bitowe zdefiniowane jako następuje (w gl2.h):
#define GL_DEPTH_BUFFER_BIT 0x00000100
#define GL_STENCIL_BUFFER_BIT 0x00000400
#define GL_COLOR_BUFFER_BIT 0x00004000Maski bitowe zawierające wiele bitów
Chociaż maski bitowe są często używane do wybierania pojedynczego bitu, można ich również używać do wybierania wielu bitów. Przyjrzyjmy się nieco bardziej skomplikowanemu przykładowi, w którym to robimy.
Urządzenia z kolorowym wyświetlaczem, takie jak telewizory i monitory, składają się z milionów pikseli, z których każdy może wyświetlić kolorową kropkę. Każda kropka koloru jest wynikiem połączenia trzech wiązek światła: czerwonej, zielonej i niebieskiej (RGB). Intensywność tych świateł zmienia się, aby uzyskać różne kolory.
Zazwyczaj intensywność R, G i B dla danego piksela jest reprezentowana przez 8-bitową liczbę całkowitą bez znaku. Na przykład czerwony piksel miałby R=255, G=0, B=0. Fioletowy piksel miałby R=255, G=0, B=255. Średnio szary piksel miałby R=127, G=127, B=127.
Podczas przypisywania wartości kolorów do piksela, oprócz R, G i B, często używana jest czwarta wartość zwana A. „A” oznacza „alfa” i określa stopień przezroczystości koloru. Jeśli A=0, kolor jest w pełni przezroczysty. Jeśli A=255, kolor jest nieprzezroczysty.
R, G, B i A są zwykle przechowywane jako pojedyncza 32-bitowa liczba całkowita, z 8 bitami na każdy składnik:
| 32-bitowa wartość RGBA | |||
| bity 31-24 | bity 23-16 | bity 15-8 | bitów 7-0 |
| RRRRRRRR | GGGGGGGG | BBBBBBBB | AAAAAAAA |
| czerwony | zielony | niebieski | alfa |
Następujący program prosi użytkownika o wprowadzenie 32-bitową wartość szesnastkową, a następnie wyodrębnia 8-bitowe wartości kolorów dla R, G, B i A.
#include <cstdint>
#include <iostream>
int main()
{
constexpr std::uint32_t redBits{ 0xFF000000 };
constexpr std::uint32_t greenBits{ 0x00FF0000 };
constexpr std::uint32_t blueBits{ 0x0000FF00 };
constexpr std::uint32_t alphaBits{ 0x000000FF };
std::cout << "Enter a 32-bit RGBA color value in hexadecimal (e.g. FF7F3300): ";
std::uint32_t pixel{};
std::cin >> std::hex >> pixel; // std::hex allows us to read in a hex value
// use Bitwise AND to isolate the pixels for our given color,
// then right shift the value into the lower 8 bits
const std::uint8_t red{ static_cast<std::uint8_t>((pixel & redBits) >> 24) };
const std::uint8_t green{ static_cast<std::uint8_t>((pixel & greenBits) >> 16) };
const std::uint8_t blue{ static_cast<std::uint8_t>((pixel & blueBits) >> 8) };
const std::uint8_t alpha{ static_cast<std::uint8_t>(pixel & alphaBits) };
std::cout << "Your color contains:\n";
std::cout << std::hex; // print the following values in hex
// reminder: std::uint8_t will likely print as a char
// we static_cast to int to ensure it prints as an integer
std::cout << static_cast<int>(red) << " red\n";
std::cout << static_cast<int>(green) << " green\n";
std::cout << static_cast<int>(blue) << " blue\n";
std::cout << static_cast<int>(alpha) << " alpha\n";
return 0;
}To daje wynik:
Enter a 32-bit RGBA color value in hexadecimal (e.g. FF7F3300): FF7F3300 Your color contains: ff red 7f green 33 blue 0 alpha
W powyższym programie używamy bitowego AND w celu sprawdzenia zestawu 8 interesujących nas bitów, a następnie w prawo przesuń je na wartość 8-bitową, abyśmy mogli wydrukować je z powrotem jako wartości szesnastkowe.
Streszczenie
Podsumowując, jak ustawiać, kasować, przełączać i wysyłać zapytania do flag bitowych:
Aby sprawdzać stany bitów, używamy bitowego AND:
if (flags & option4) ... // if option4 is set, do somethingAby ustawić bity (włączyć), używamy bitowego LUB:
flags |= option4; // turn option 4 on.
flags |= (option4 | option5); // turn options 4 and 5 on.Aby wyczyścić bity (wyłączyć), używamy bitowego AND z bitowego NOT:
flags &= ~option4; // turn option 4 off
flags &= ~(option4 | option5); // turn options 4 and 5 offAby odwrócić stany bitów, używamy bitowego XOR:
flags ^= option4; // flip option4 from on to off, or vice versa
flags ^= (option4 | option5); // flip options 4 and 5Czas quizu
Pytanie nr 1
Nie używaj std::bitset w tym quizie. Używamy tylko std::bitset do drukowania.
Biorąc pod uwagę następujący program:
#include <bitset>
#include <cstdint>
#include <iostream>
int main()
{
[[maybe_unused]] constexpr std::uint8_t option_viewed{ 0x01 };
[[maybe_unused]] constexpr std::uint8_t option_edited{ 0x02 };
[[maybe_unused]] constexpr std::uint8_t option_favorited{ 0x04 };
[[maybe_unused]] constexpr std::uint8_t option_shared{ 0x08 };
[[maybe_unused]] constexpr std::uint8_t option_deleted{ 0x10 };
std::uint8_t myArticleFlags{ option_favorited };
// Place all lines of code for the following quiz here
std::cout << std::bitset<8>{ myArticleFlags } << '\n';
return 0;
}a) Dodaj linię kodu, aby ustawić wyświetlany artykuł.
Oczekiwany wynik:
00000101
b) Dodaj linię kodu, aby sprawdzić, czy artykuł został usunięty.
c) Dodaj linię kodu, aby wyczyścić artykuł jako ulubiony.
Oczekiwany wynik (Zakładając, że wykonałeś quiz (a)):
00000001
1d) Dodatkowy kredyt: dlaczego poniższe dwa wiersze są identyczne?
myflags &= ~(option4 | option5); // turn options 4 and 5 off
myflags &= ~option4 & ~option5; // turn options 4 and 5 off
