소년코딩

03.08 - 비트 플래그와 비트 마스크 (Bit flags and bit masks)

비트 플래그 (bit flag)

메모리의 최소 크기 단위는 1바이트이므로 변수의 크기는 적어도 1바이트 이상이다. 8비트(1바이트)는 비트가 8개이므로 8가지 상태를 저장할 수 있다. 이는 1바이트를 사용해서 1비트만 사용하고 7비트를 낭비함으로써 1가지 상태만 저장하는 bool 자료형보다 훨씬 효율적이다.

플래그(flag)는 깃발에서 유래한 용어다. 보통 깃발을 위로 올리면 on, 아래로 내리면 off를 뜻한다. 이걸 정수의 비트에 활용하는 건데 비트가 1이면 on, 0이면 off를 나타낸다.

여기서 바이트의 개별 비트를 비트 플래그(bit flag)라고 한다.

플래그를 설명할 때는 일반적으로 오른쪽에서 왼쪽으로 센다. 예를 들어, 아래는 두 번째와 여덟 번째 비트가 켜진 상태다.

0100 0001    // 두 번째 비트와 여덟 번째 비트가 켜진 상태(on)

C++ 14에서 비트 플래그 정의 (Defining bit flags in C++14)

비트 플래그를 사용하려면 바이트 내에서 개별 비트를 식별할 수 있는 방법을 사용해서 비트를 조작해야 한다. 해당 비트를 나타내는 기호 상수를 정의하는 방식으로 조작할 수 있다.

C++ 14는 바이너리 리터럴을 제공한다:
// Define 8 separate bit flags (these can represent whatever you want)
const unsigned char option0 = 0b0000'0001; // represents bit 0
const unsigned char option1 = 0b0000'0010; // represents bit 1
const unsigned char option2 = 0b0000'0100; // represents bit 2 
const unsigned char option3 = 0b0000'1000; // represents bit 3
const unsigned char option4 = 0b0001'0000; // represents bit 4
const unsigned char option5 = 0b0010'0000; // represents bit 5
const unsigned char option6 = 0b0100'0000; // represents bit 6
const unsigned char option7 = 0b1000'0000; // represents bit 7

이제 각 비트 위치를 나타내는 일련의 기호 상수가 있다. 이 기호 상수를 비트를 조작하는 데 사용할 수 있다.


C++ 11 또는 이전 버전에서 비트 플래그 정의 (Defining bit flags in C++11 or earlier)

C++ 11은 바이너리 리터럴을 제공하지 않으므로 기호 상수를 설정하는 데 다른 방법을 사용해야 한다.

16진수를 사용하는 게 일반적인 방법이다:
// Define 8 separate bit flags (these can represent whatever you want)
const unsigned char option0 = 0x1;  // hex for 0000 0001 
const unsigned char option1 = 0x2;  // hex for 0000 0010
const unsigned char option2 = 0x4;  // hex for 0000 0100
const unsigned char option3 = 0x8;  // hex for 0000 1000
const unsigned char option4 = 0x10; // hex for 0001 0000
const unsigned char option5 = 0x20; // hex for 0010 0000
const unsigned char option6 = 0x40; // hex for 0100 0000
const unsigned char option7 = 0x80; // hex for 1000 0000
읽기는 조금 어렵지만, 왼쪽 시프트 연산자(<<)를 사용한 더 쉬운 방법도 있다:
// Define 8 separate bit flags (these can represent whatever you want)
const unsigned char option0 = 1 << 0; // 0000 0001 
const unsigned char option1 = 1 << 1; // 0000 0010
const unsigned char option2 = 1 << 2; // 0000 0100
const unsigned char option3 = 1 << 3; // 0000 1000
const unsigned char option4 = 1 << 4; // 0001 0000
const unsigned char option5 = 1 << 5; // 0010 0000
const unsigned char option6 = 1 << 6; // 0100 0000
const unsigned char option7 = 1 << 7; // 1000 0000

플래그를 사용한 비트 조작 (Using bit flags to manipulate bits)

다음으로 필요한 것은 조작하고자 하는 변수다. 사용하는 옵션(option) 수에 따라 적절한 크기(8비트, 16비트, 32비트 등)의 부호 없는(unsigned) 정수 자료형을 사용한다.

// 위에서 정의한 8가지 옵션을 위해 8비트를 사용한다.
unsigned char myflags = 0; // all bits turned off to start

비트 켜기 (Turning individual bits on)

비트 OR 연산자(|)를 사용해 비트를 켤 수 있다.

myflags |= option4; // turn option 4 on
myflags |= option4myflags = (myflags | option4)와 같다:
myflags = 0000 0000 (we initialized this to 0)
option4 = 0001 0000
-------------------
result  = 0001 0000

비트 끄기 (Turning individual bits off)

비트 AND 연산자(&)와 비트 NOT 연산자(~)를 이용해서 비트를 끌 수 있다.

myflags &= ~option4; // turn option 4 off

myflags가 처음에 0001 1100이라고 가정하자. (option3, 4 및 5가 켜진 상태)

myflags &= ~option4myflags = (myflags & ~option4)와 같다:
myflags  = 0001 1100
~option4 = 1110 1111
--------------------
result   = 0000 1100

그래서 0000 1100이 다시 할당된다. 즉, 네 번째 비트를 끈 것이다.

여러 비트를 동시에 끌 수도 있다.

myflags &= ~(option4 | option5); // turn options 4 and 5 off at the same time

비트 뒤집기 (Flipping individual bits)

비트 XOR 연산자(^)를 이용해서 비트를 토글(toggle)할 수 있다.

myflags ^= option4; // flip option4 from on to off, or vice versa
myflags ^= (option4 | option5); // flip options 4 and 5 at the same time

비트가 켜져 있는지 꺼져 있는지 확인하기 (Determining if a bit is on or off)

비트 AND 연산자(&)를 이용해서 비트의 상태를 알 수 있다.

if (myflags & option4)
    std::cout << "myflags has option 4 set";
if !(myflags & option5)
    std::cout << "myflags does not have option 5 set";

비트 플래그가 유용한 이유 (Why are bit flags useful?)

1. 옵션(or 상태)가 많이 필요할 때:

myflag 같은 bool 자료형 변수가 하나가 아니라 myflag1, myflag2 ... 처럼 n개의 옵션이 필요하다고 가정해보자. 8개의 옵션(or 상태)을 정의하려면 각각 true, false가 필요하므로 16개의 bool 자료형 변수를 정의해야 하고, 16바이트 메모리를 사용한다 (옵션이 많을수록 더 많은 메모리를 사용한다) 그러나 비트 플래그를 사용하면 8개의 옵션을 사용할 때, 1바이트(8비트)로 충분하다. 즉, 메모리를 절약할 수 있다.

2. 옵션(or 상태)을 조합할 때:

32가지 옵션을 이용해 사용할 수 있는 함수가 있다고 가정해보자. 이 함수를 호출하려면 32개의 매개 변수(parameter)를 사용할 것이다.

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);

매개 변수 이름을 보고 옵션의 기능을 유추할 수 없을뿐더러 매개 변수의 목록이 너무 많다.

option10option32true로 설정된 함수를 호출하려면 다음과 같이 해야한다.

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);

읽기 매우 어렵고 어떤 매개 변수가 어떤 옵션에 해당하는지 기억해야 한다. 또한, 호출할 때마다 32개의 bool 값을 복사해야 하므로 성능이 안 좋을 수 있다.

대신, 다음과 같이 비트 플래그를 사용하여 함수를 정의하면:

void someFunction(unsigned int options);

비트 플래그를 이용해서 원하는 옵션만 전달할 수 있다.

someFunction(option10 | option32);

이 같은 방법은 읽기 훨씬 더 쉬울 뿐만 아니라, 2개의 연산(비트 OR 연산 1개와 파라미터 복사본 1개)만 포함하기 때문에 더 성능이 좋다.

또한, 나중에 옵션을 추가해야 할 때에는 비트 플래그를 정의하기만 하면 된다.


Bit flags in real life

3D 그래픽 라이브러리인 'OpenGL'에서 몇몇 함수는 비트 플래그를 매개 변수로 사용한다.

glClearg (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // clear the color and the depth buffer
GL_COLOR_BUFFER_BITGL_DEPTH_BUFFER_BIT는 다음과 같이 정의되어 있다:
#define GL_DEPTH_BUFFER_BIT               0x00000100
#define GL_STENCIL_BUFFER_BIT             0x00000400
#define GL_COLOR_BUFFER_BIT               0x00004000
아래는 게임에 대한 추상적인 코드다:
#include <iostream>
int main()
{
    // Define a bunch of physical/emotional states
    const unsigned char isHungry = 1 << 0;   // 0000 0001
    const unsigned char isSad = 1 << 1;      // 0000 0010
    const unsigned char isMad = 1 << 2;      // 0000 0100
    const unsigned char isHappy = 1 << 3;    // 0000 1000
    const unsigned char isLaughing = 1 << 4; // 0001 0000
    const unsigned char isAsleep = 1 << 5;   // 0010 0000
    const unsigned char isDead = 1 << 6;     // 0100 0000
    const unsigned char isCrying = 1 << 7;   // 1000 0000

    unsigned char me = 0; // 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 rather than an integer)
    std::cout << "I am happy? " << static_cast<bool>(me & isHappy) << '\n';
    std::cout << "I am laughing? " << static_cast<bool>(me & isLaughing) << '\n';

    return 0;
}

std::bitset

C++ 표준 라이브러리는 비트 플래그 조작을 돕는 std::biset을 제공한다.

std::bitset을 사용하려면 <bitset> 헤더 파일을 #include 한 다음 필요한 비트 수를 나타내는 std::bitset 변수를 정의해야 한다.

#include <bitset>

std::bitset<8> bits; // we need 8 bits

필요한 경우 초기값 집합을 사용하여 비트 집합을 초기화할 수 있다.

#include <bitset>

std::bitset<8> bits(option1 | option2) ; // start with option 1 and 2 turned on
std::bitset<8> morebits(0x3) ; // start with bit pattern 0000 0011

초기값은 이진수로 해석되므로 값 30000 0011이 된다.

std:bitset은 4가지 주요 함수를 제공한다.

  • test(): 비트의 상태를 알려준다.
  • set(): 비트를 켠다.
  • reset(): 비트를 끈다.
  • flip(): 비트를 뒤집는다.
#include <bitset>
#include <iostream>

// Note that with std::bitset, our options correspond to bit indices, not bit patterns
const int option0 = 0;
const int option1 = 1;
const int option2 = 2;
const int option3 = 3;
const int option4 = 4;
const int option5 = 5;
const int option6 = 6;
const int option7 = 7;

int main()
{
    std::bitset<8> bits(0x2); // we need 8 bits, start with bit pattern 0000 0010
    bits.set(option4); // set bit 4 to 1 (now we have 0001 0010)
    bits.flip(option5); // flip bit 5 (now we have 0011 0010)
    bits.reset(option5); // set bit 5 back to 0 (now we have 0001 0010)

    std::cout << "Bit 4 has value: " << bits.test(option4) << '\n';
    std::cout << "Bit 5 has value: " << bits.test(option5) << '\n';
    std::cout << "All the bits: " << bits << '\n';

    return 0;
}

This prints:

Bit 4 has value: 1
Bit 5 has value: 0
All the bits: 00010010

std::bitsetstd::cout을 이용해 출력하면 모든 비트 값이 출력된다.

std:bitset은 표준 비트 단위 연산자(|, &, ^)도 지원하므로 원하는 경우 사용할 수 있다. bitset이 더 편리하고 오류 발생률이 낮으므로 사용하는 것을 추천한다.


비트 마스크 (bit mask)

비트 플래그의 원칙은 비트 단위 연산 한 번으로 여러 비트를 한 번에 켜거나, 끄거나, 뒤집거나, 검사하도록 확장할 수 있다.

플래그의 비트를 조작하거나 검사할 때 사용하는 숫자를 비트 마스크(bit mask)라고 부른다.

비트 마스크를 사용한 예제를 보자. 다음 예제는 사용자에게 숫자를 입력받는다. 그런 다음 비트 마스크를 사용하여 하위 4비트 만 유지한다.

#include <iostream>

int main()
{
    const unsigned int lowMask = 0xF; // bit mask to keep low 4 bits (hex for 0000 0000 0000 1111)

    std::cout << "Enter an integer: ";
    int num;
    std::cin >> num;

    num &= lowMask; // remove the high bits to leave only the low bits

    std::cout << "The 4 low bits have value: " << num << '\n';

    return 0;
}

Enter an integer: 151
The 4 low bits have value: 7

151은 바이너리(binary)로 1001 0111이다. lowMask는 8비트 바이너리로 0000 1111이다. 1001 0111 & 0000 1111 = 0000 0111이므로 10진수 7을 출력한다.

위 예제는 보기에 매우 인위적이지만, 한 번의 작업으로 여러 개의 비트를 수정할 수 있다는 의미가 중요하다.


RGBA Color Example

TV와 모니터 같은 컬러 디스플레이 장치는 각각 색 점을 표시할 수 있는 수백만 개의 픽셀로 구성되어 있다. 색상의 점은 세개의 광선(빨간 색, 녹색, 파란 색: RGB)으로 구성된다. 색상 강도를 변경하면 색상 스펙트럼의 모든 색상을 만들 수 있다. 일반적으로 특정 픽셀의 R, G, B양은 8비트 기호 없는(unsgiend) 정수로 표시된다. 예를 들면, 빨간 색: (R: 255, G: 0, B: 0), 보라 색: (R: 255, G: 0, B: 255), 회 색: (R=127, G=127, B=127)이 있다.

픽셀에 색을 할당할 때 RGB와 함께 4번 째 값 'A'도 쓰인다. A는 'Alpha'를 의미하며 색이 얼마나 투명한지를 제어한다. A=0이면 완전히 투명하고, A=255면 완전히 불투명하다.

R, G, B 및 A는 일반적으로 32비트 정수(각 구성 요소에 8비트 사용)에 저장된다.

32-bit RGBA value
bits 31-24 bits 23-16 bits 15-8 bits 7-0
RRRRRRRR GGGGGGGG BBBBBBBB AAAAAAAA
red green blue alpha

다음 프로그램은 사용자로부터 32비트 16진수 값을 입력받아서 R, G, B 및 A의 8비트 색 값을 추출한다.

#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 allows us to read in a hex value

    // use bitwise AND to isolate red pixels, then right shift the value into the range 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

비트 AND 연산자 (&)를 사용해 필요한 8비트 집합을 검사한 다음 오른쪽 시프트 연산자 (>>)를 이용해 0~255 범위로 이동시킨다.


요약 (Summary)

비트 플래그 켜기, 끄기, 뒤집기, 검사하기 요약:

비트 AND 연산자(&)를 이용해서 비트의 상태를 알 수 있다.

if (myflags & option4) ... // if option4 is set, do something

비트 OR 연산자(|)를 사용해 비트를 켤 수 있다.

myflags |= option4; // turn option 4 on.
myflags |= (option4 | option5); // turn options 4 and 5 on.

비트 AND 연산자(&)와 비트 NOT 연산자(~)를 이용해서 비트를 끌 수 있다.

myflags &= ~option4; // turn option 4 off
myflags &= ~(option4 | option5); // turn options 4 and 5 off

비트 XOR 연산자(^)를 이용해서 비트를 토글(toggle)할 수 있다.

myflags ^= option4; // flip option4 from on to off, or vice versa
myflags ^= (option4 | option5); // flip options 4 and 5

cpp 이 포스트의 원문은 http://www.learncpp.com/cpp-tutorial/3-8a-bit-flags-and-bit-masks/ 입니다.

댓글 로드 중…

블로그 정보

소년코딩 - 소년코딩

소년코딩, 자바스크립트, C++, 물리, 게임 코딩 이야기

최근에 게시된 이야기