24.4 비트 연산자로 플래그 처리하기

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

다음과 같이 8비트(1바이트) 크기의 자료형은 비트가 8개가 들어가므로 8가지 상태를 저장할 수 있습니다. 여기서는 두 번째 비트와 여덟 번째 비트가 켜진 상태입니다.

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

그렇다면 int와 같은 4바이트 크기의 자료형은 몇 가지 상태를 저장할 수 있을까요? 4바이트는 32비트이므로 32개의 상태를 저장할 수 있습니다.

참고 | 플래그를 사용하는 곳은?

그냥 int형 변수 8개를 선언하여 각각 상태를 저장하면 간단할 텐데 플래그를 사용하는 이유는 무엇일까요? 플래그는 적은 공간에 정보를 저장해야 하고, 빠른 속도가 필요할 때 사용합니다. 가장 대표적인 장치가 CPU인데요. CPU는 내부 저장 공간이 매우 작기 때문에 각종 상태를 비트로 저장합니다.

먼저 특정 비트를 켜는 방법을 알아보겠습니다. 다음 내용을 소스 코드 편집 창에 입력한 뒤 실행해보세요.

  • 플래그 |= 마스크

bitwise_flag_on.c

#include <stdio.h>
 
int main()
{
    unsigned char flag = 0;
 
    flag |= 1;    // 0000 0001 마스크와 비트 OR로 여덟 번째 비트를 켬
    flag |= 2;    // 0000 0010 마스크와 비트 OR로 일곱 번째 비트를 켬
    flag |= 4;    // 0000 0100 마스크와 비트 OR로 여섯 번째 비트를 켬
 
    printf("%u\n", flag);    // 7: 0000 0111
 
    if (flag & 1)    // & 연산자로 0000 0001 비트가 켜져 있는지 확인
        printf("0000 0001은 켜져 있음\n");
    else
        printf("0000 0001은 꺼져 있음\n");
 
    if (flag & 2)    // & 연산자로 0000 0010 비트가 켜져 있는지 확인
        printf("0000 0010은 켜져 있음\n");
    else
        printf("0000 0010은 꺼져 있음\n");
 
    if (flag & 4)    // & 연산자로 0000 0100 비트가 켜져 있는지 확인
        printf("0000 0100은 켜져 있음\n");
    else
        printf("0000 0100은 꺼져 있음\n");
 
    return 0;
}

실행 결과

7
0000 0001은 켜져 있음
0000 0010은 켜져 있음
0000 0100은 켜져 있음

플래그로 사용할 변수에 |= 연산자와 숫자를 사용하여 특정 비트를 켭니다. 여기서 플래그의 비트를 조작하거나 검사할 때 사용하는 숫자를 마스크(mask)라고 부릅니다. 예제에서는 1, 24가 마스크입니다.

flag |= 1;    // 0000 0001 마스크와 비트 OR로 여덟 번째 비트를 켬
flag |= 2;    // 0000 0010 마스크와 비트 OR로 일곱 번째 비트를 켬
flag |= 4;    // 0000 0100 마스크와 비트 OR로 여섯 번째 비트를 켬

플래그의 비트를 켜는 동작은 비트 OR 연산의 특성을 활용한 것인데 0 | 11 | 1은 1이므로 flag의 비트가 꺼져있으면 비트를 켜고, 켜져 있으면 그대로 유지합니다.

그림 24‑9 플래그의 비트 켜기

플래그의 특정 비트가 켜져 있는지 검사하려면 & 연산자를 사용하면 됩니다.

if (flag & 4)    // & 연산자로 0000 0100 비트가 켜져 있는지 확인
    printf("0000 0100은 켜져 있음\n");
else
    printf("0000 0100은 꺼져 있음\n");

& 연산자는 두 비트가 모두 1이라야 1입니다. 따라서 flag에 저장된 0000 0111과 마스크 값 0000 0100(4)&로 연산하면 여섯 번째 비트가 1이 됩니다. 연산 결과가 마스크 값이 나오면 비트가 켜져 있는 것이고, 0이 나오면 꺼져있는 것이죠.

0000 0111
0000 0100 마스크
_________ &
0000 0100

이번에는 플래그의 비트를 끄는 방법입니다.

  • 플래그 &= ~마스크

bitwise_flag_off.c

#include <stdio.h>
 
int main()
{
    unsigned char flag = 7;    // 7: 0000 0111
 
    flag &= ~2;    // 1111 1101 마스크 값 2의 비트를 뒤집은 뒤 비트 AND로 일곱 번째 비트를 끔
 
    printf("%u\n", flag);    // 5: 0000 0101
 
    if (flag & 1)    // & 연산자로 0000 0001 비트가 켜져 있는지 확인
        printf("0000 0001은 켜져 있음\n");
    else
        printf("0000 0001은 꺼져 있음\n");
 
    if (flag & 2)    // & 연산자로 0000 0010 비트가 켜져 있는지 확인
        printf("0000 0010은 켜져 있음\n");
    else
        printf("0000 0010은 꺼져 있음\n");
 
    if (flag & 4)    // & 연산자로 0000 0100 비트가 켜져 있는지 확인
        printf("0000 0100은 켜져 있음\n");
    else
        printf("0000 0100은 꺼져 있음\n");
 
    return 0;
}

실행 결과

5
0000 0001은 켜져 있음
0000 0010은 꺼져 있음
0000 0100은 켜져 있음

마스크 값을 ~ 연산자로 비트를 뒤집은 뒤 &= 연산자를 사용하여 특정 비트를 끕니다.

flag &= ~2;    // 1111 1101 마스크 값 2의 비트를 뒤집은 뒤 비트 AND로 일곱 번째 비트를 끔

먼저 마스크 값 2의 비트를 뒤집습니다.

0000 0010
_________ ~
1111 1101

이렇게 하면 끄고자 하는 비트 이외의 값은 모두 1이 됩니다. 그리고 flag에 마스크 값의 비트를 뒤집은 값으로 & 연산하면 비트를 끌 수 있습니다.

그림 24‑10 플래그의 비트 끄기

즉, 1111 1101에서 1flag의 원래 있던 비트 값을 유지합니다. 비트 AND 연산이므로 0이었다면 그대로 0이되고, 1이었다면 그대로 1이 됩니다. 그리고 1111 1101에서 0은 비트 AND 연산을 했을 때 원래 비트가 1이든 0이든 항상 0이되므로 원하는 비트를 끄게 됩니다.

마지막으로 비트가 켜져 있다면 끄고, 꺼져있다면 켜는 방법입니다. 다른 말로는 토글(toggle)이라고도 합니다.

  • 플래그 ^= 마스크

bitwise_flag_toggle.c

#include <stdio.h>
 
int main()
{
    unsigned char flag = 7;    // 7: 0000 0111
 
    flag ^= 2;    // 0000 0010 마스크와 비트 XOR로 일곱 번째 비트를 토글
    flag ^= 8;    // 0000 1000 마스크와 비트 XOR로 다섯 번째 비트를 토글
 
    printf("%u\n", flag);    // 13: 0000 1101
 
    if (flag & 1)    // & 연산자로 0000 0001 비트가 켜져 있는지 확인
        printf("0000 0001은 켜져 있음\n");
    else
        printf("0000 0001은 꺼져 있음\n");
 
    if (flag & 2)    // & 연산자로 0000 0010 비트가 켜져 있는지 확인
        printf("0000 0010은 켜져 있음\n");
    else
        printf("0000 0010은 꺼져 있음\n");
 
    if (flag & 4)    // & 연산자로 0000 0100 비트가 켜져 있는지 확인
        printf("0000 0100은 켜져 있음\n");
    else
        printf("0000 0100은 꺼져 있음\n");
 
    if (flag & 8)    // & 연산자로 0000 1000 비트가 켜져 있는지 확인
        printf("0000 1000은 켜져 있음\n");
    else
        printf("0000 1000은 꺼져 있음\n");
 
    return 0;
}

실행 결과

13
0000 0001은 켜져 있음
0000 0010은 꺼져 있음
0000 0100은 켜져 있음
0000 1000은 켜져 있음

^= 연산자와 마스크 사용하여 특정 비트가 켜져 있으면 끄고, 꺼져 있으면 켭니다.

flag ^= 2;    // 0000 0010 마스크와 비트 XOR로 일곱 번째 비트를 토글
flag ^= 8;    // 0000 1000 마스크와 비트 XOR로 다섯 번째 비트를 토글

플래그의 비트를 토글하는 동작은 비트 XOR 연산의 특성을 활용한 것입니다. 두 비트가 다르면 1, 같으면 0이죠. 따라서 flag의 비트가 1이라면 마스크의 1과 같으므로 0이되고, 0이라면 마스크의 1과 다르므로 1이 되는 원리입니다.

그림 24‑11 플래그의 비트 토글하기

여기서는 0000 0111에서 일곱 번째 비트를 토글하고, 다섯 번째 비트를 토글했으므로 0000 1101이 됩니다.

0000 0111
0000 0010 마스크
_________ ^
0000 0101
0000 1000 마스크
_________ ^
0000 1101

지금까지 비트 연산자의 고급 사용 방법을 알아보았는데 현직 프로그래머들도 이런 내용을 모르는 경우가 많습니다. 내용이 어렵기도 하고 쓰이는 곳이 그렇게 많지 않기 때문입니다. 나중에 리눅스 커널의 소스 코드를 보거나 하드웨어를 다룰 때 플래그 처리 부분이 나오면 다시 돌아와서 찾아보면 됩니다.