Skip to content

19. 비트 연산자 응용


1. 키워드

  • 비트 연산자
  • 플래그(Flag), 마스크(Mask), 토글(Toggle)
  • 최상위 비트(MSB: Most Significant Bit)와 최하위 비트(LSB: Least Significant Bit)


2. 비트 연산자 응용하기

  • C의 자료형은 부호 있는 정수와 부호 없는 정수 두 가지가 있다.
  • 두 자료형에 비트 연산을 했을 때 어떤 차이점이 있는지 알아보자.
  • 또한, 비트 연산자를 응용한 플래그 처리 방법도 알아보자.


3. 시프트 연산과 2의 거듭제곱 알아보기

  • 시프트 연산자는 2의 거듭제곱인 숫자를 빠르게 구할 때 유용하다.


#include <stdio.h>

int main()
{
    unsigned char num1 = 1; //   1: 0000 0001

    printf("%u\n", num1 << 1); // 0000 0010: 2
    printf("%u\n", num1 << 2); // 0000 0100: 22
    printf("%u\n", num1 << 3); // 0000 1000: 23
    printf("%u\n", num1 << 4); // 0001 0000: 24
    printf("%u\n", num1 << 5); // 0010 0000: 25
    printf("%u\n", num1 << 6); // 0100 0000: 26
    printf("%u\n", num1 << 7); // 1000 0000: 27

    return 0;
}

// 2
// 4
// 8
// 16
// 32
// 64
// 128


  • 0000 0001을 왼쪽으로 한 번씩 이동하면 2의 거듭제곱으로 수가 늘어난다.
  • 즉, 비트의 각 자릿수는 2의 거듭제곱을 뜻하므로 비트의 이동 횟수는 지수(Exponent)라 할 수 있다.
  • 예를 들면 1 << 3\(2^3\)과 같다.


4. 시프트 연산으로 자릿수를 넘어서는 경우 알아보기

  • 지금까지 시프트 연산자를 사용할 때 주어진 자료형 안에서 왼쪽, 오른쪽으로 이동했다.
  • 그렇다면 시프트 연산자를 사용하여 비트가 첫째 자리나 마지막 자리를 넘어설 때를 확인해 보자.


#include <stdio.h>

int main()
{
    unsigned char num1 = 240; // 240: 1111 0000
    unsigned char num2 = 15;  //  15: 0000 1111

    unsigned char num3, num4;

    num3 = num1 << 2; // num1의 비트 값을 왼쪽으로 2번 이동
    num4 = num2 >> 2; // num2의 비트 값을 오른쪽으로 2번 이동

    printf("%u\n", num3); // 1100 0000: 맨 앞의 11이 사라져서 11000000이 됨
    printf("%u\n", num4); // 0000 0011: 맨 뒤의 11이 사라져서 00000011이 됨

    return 0;
}

// 192
// 3


  • 1111 0000(240)을 왼쪽으로 2번 이동시키면 맨 앞의 11은 사라져서 1100 0000(192)이 된다.


1111 0000(240)
_________ << 2
1100 0000(192)


  • 마찬가지로 0000 1111(15)을 오른쪽으로 2번 이동시키면 맨 뒤의 11은 사라져서 0000 0011(3)이 된다.


0000 1111(15)
_________ >> 2
0000 0011(3)


  • 즉, 비트에서 첫째 자리나 마지막 자리를 넘어서는 비트는 그대로 사라진다.


최상위 비트, 최하위 비트

  • 비트에서 첫 번째 비트를 최상위 비트, 마지막 비트를 최하위 비트라고 부른다.


001


5. 부호 있는 자료형의 비트 연산 알아보기

  • 지금까지 부호 없는(unsigned) 자료형으로 비트 연산을 했다.
  • 하지만 부호 있는 자료형을 비트 연산할 때는 부호 비트를 조심해야 한다.


  • 먼저 부호 없는 자료형과 부호 있는 자료형에 >> 연산을 해보자.


#include <stdio.h>

int main()
{
    unsigned char num1 = 131; //  131: 1000 0011
    char num2 = -125;         // -125: 1000 0011

    unsigned char num3;
    char num4;

    num3 = num1 >> 5; // num1의 비트 값을 오른쪽으로 5번 이동
    num4 = num2 >> 5; // num2의 비트 값을 오른쪽으로 5번 이동

    printf("%u\n", num3); // 0000 0100: 맨 뒤의 11은 사라지고 0000 0100이 됨
    printf("%d\n", num4); // 1111 1100: 모자라는 공간은 부호 비트의 값인 1로 채워지므로 1111 1100이 됨

    return 0;
}

// 4
// -4


  • 1바이트짜리 부호 없는 변수에 131과 부호 있는 변수에 -125를 할당했다.
  • 부호 없는 변수의 1000 0011(131)을 오른쪽으로 5번 이동시켰을 때는 0000 0100(4)가 나왔는데 부호 있는 변수의 1000 0011(-125)를 오른쪽으로 5번 이동시키니 1111 1100(-4)가 나왔다.


1000 0011(131)
_________ >> 5
0000 0100(4)
1000 0011(-125)
_________ >> 5
1111 1100(-4)


  • 부호 있는 자료형의 첫 번째 비트는 부호 비트라고 하는데, 이 비트가 1이면 음수, 0이면 양수이다.


002


  • 부호 있는 자료형에 저장된 1000 0011은 첫 번째 비트가 1이므로 음수이고 10진수로는 -125가 된다.
  • 이 비트들을 오른쪽으로 5번 이동시키면 모자라는 공간은 모두 부호 비트의 값으로 채워지기 때문에 1111 1100(-4)가 된다.
  • 하지만 부호 없는 자료형은 비트를 오른쪽으로 이동해도 모자라는 공간은 모두 0으로 채워진다.
  • 즉, 비트 연산자는 부호 있는 자료형과 부호 없는 자료형이 다르게 동작한다.


003


  • 그러면 부호 있는 자료형에서 부호 비트가 0인 양수에 >> 연산을 하면 어떻게 되는지 확인해 보자.


#include <stdio.h>

int main()
{
    char num1 = 67; // 67: 0100 0011
    char num2;

    num2 = num1 >> 5; // num1의 비트 값을 오른쪽으로 5번 이동

    printf("%d\n", num2); // 0000 0010: 모자라는 공간은 부호 비트의 값인 0으로 채워지므로 0000 0010이 됨

    return 0;
}

// 2


  • 부호 있는 자료형에 저장된 0100 0011(67)은 부호 비트가 0이다.
  • 따라서 오른쪽으로 5번 이동했을 때 모자라는 공간은 부호 비트의 값인 0으로 채워지므로 0000 0010(2)이 된다.


004


  • 이번에는 부호 있는 자료형에서 << 연산을 해보자.


#include <stdio.h>

int main()
{
    char num1 = 113; //  113: 0111 0001
    char num2 = -15; //  -15: 1111 0001
    char num3, num4, num5, num6;

    num3 = num1 << 2; // num1의 비트 값을 왼쪽으로 2번 이동
    num4 = num2 << 2; // num2의 비트 값을 왼쪽으로 2번 이동

    num5 = num1 << 4; // num1의 비트 값을 왼쪽으로 4번 이동
    num6 = num2 << 4; // num1의 비트 값을 왼쪽으로 4번 이동

    printf("%d\n", num3); // 1100 0100: 부호 비트를 덮어쓰게 되므로 양수에서 음수가 됨
    printf("%d\n", num4); // 1100 0100: 이미 음수인 수는 계속 음수가 됨

    printf("%d\n", num5); // 0001 0000: 이미 양수인 수는 계속 양수가 됨
    printf("%d\n", num6); // 0001 0000: 부호 비트를 덮어쓰게 되므로 음수에서 양수가 됨

    return 0;
}

// -60
// -60
// 16
// 16


  • 부호 있는 자료형에서 첫 번째 비트가 0인 양수 0111 0001(113)을 왼쪽으로 2번 이동시키면 1이 부호 비트를 덮어쓰게 된다(오버플로우 상황).
  • 따라서 1100 0100(-60)이 되고, 양수였던 수가 음수가 되어버린다.


005


  • 이미 첫 번째 비트가 1인 음수 1111 0001(-15)는 왼쪽으로 2번 이동시켜서 부호 비트를 덮어쓰더라도 부호 비트는 바뀌지 않으므로 계속 음수 1100 0100(-60)이 된다.


006


  • 비트를 왼쪽으로 좀 더 이동시켜서 0이 부호 비트를 덮어쓰게 만들면 음수였던 수는 양수가 된다.
  • 즉, 음수인 1111 0001(-15)를 왼쪽으로 4번 이동시키면 1은 모두 사라지고, 부호 비트에 0이 와서 양수 0001 0000(16)이 된다.


007


  • 이미 첫 번째 비트가 0인 양수 0111 0001(113)을 왼쪽으로 4번 이동시키면 앞의 1은 모두 사라지고, 부호 비트에 0이 오므로 계속 양수 0001 0000(16)이 된다.


008


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

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


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


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


  • 그렇다면 int와 같은 4바이트 크기의 자료형은 32비트이므로 32개의 상태를 저장할 수 있다.


플래그를 사용하는 곳은?

  • 플래그는 적은 공간에 정볼르 저장해야 하고, 빠른 속도가 필요할 때 사용한다.
  • 가장 대표적인 장치가 CPU인데, CPU는 내부 저장 공간이 매우 작기 때문에 각종 상태를 비트로 저장한다.


  • 먼저 특정 비트를 켜는 방법을 알아보자.


#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); // 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은 켜져 있음


  • 플래그로 사용할 변수에 |= 연산자와 숫자를 사용하여 특정 비트를 켠다.
  • 여기서 플래그의 비트를 조작하거나 검사할 때 사용하는 숫자를 마스크라고 부른다.
  • 예제에서는 1, 2, 4가 마스크이다.


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


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


009


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


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


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


#include <stdio.h>

int main()
{
    unsigned char flag = 7; // 7: 0000 0111

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

    printf("%u\n", flag); // 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에 마스크 값의 비트를 뒤집은 값으로 & 연산하면 비트를 끌 수 있다.


010


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


  • 마지막으로 비트가 켜져 있다면 끄고, 꺼져 있다면 켜는 방법인데, 다른 말로는 토글이라고도 한다.


#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); // 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이 되는 원리이다.


011


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


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

References