본문 바로가기
<CPP>/BASIC

17. 비트연산자(Bitwise Operator)

by CodeGrimie 2021. 1. 20.

모든 컴퓨터가 0과 1로만 계산하는 건 아니다.

여전히 거의 대부분의 컴퓨터는 01만으로 모든 것을 계산한다.

 

거의 대부분이라 범위를 한정하는 이유는

우리나라가 3진법, 4진법을 사용하는 반도체 기술을 발표했기 때문이다.

 

3진법은 성균관 대학교(박진홍 교수 연구팀, 2016)에서 소자 회로 기술을 공개했고

이후 울산과학기술원(김경록 교수 연구팀, 2019)에서 실제 대면적 웨이퍼에 구현하는 데 성공했다.

 

4진법은 성균관 대학교(박진홍 교수 연구팀, 2019)에서 소자회로 기술을 공개했다.

 

하지만 우리가 주변에서 흔히 볼 수 있는 컴퓨터는 아니니 0과 1 계산을 중심으로 생각해보자.

이 0과 1만으로 이루어진 수체계를 2진수(Binary Digit)라 하는데 이를 줄여서 Bit(비트)라고 부른다.

(여담으로 이 비트는 드랍 더 비트 할 때 그 비트가 맞다.)

 

C언어가 제공하는 변수형들로도 대부분의 계산이 가능하다.

 

하지만 경우에 따라서는 프로그래머가 직접 이 Bit를 연산할 수 있어야 하기 때문에

C언어는 Bit를 연산할 수 있는 연산자를 제공한다.

비트 연산자(Bitwise Operator)

비트 연산자는 Bit 단위로 연산하기 때문에 이냐 아니냐의 개념으로 접근한다.

&(AND)

▼ 두 Bit가 같다면 1을 반환한다.

#include <cstdio>

int main()
{
    if (1 & 1)
        printf("It's 1!\n");
        
    if (1 & 0)
        printf("It's 0!\n");
        
    return (0);
}

보통 두 Bit가 동일한지 확인 할 때 많이 사용된다.

|(OR)

▼ 두 Bit 중 하나라도 1이면 1을 반환한다.

#include <cstdio>

int main()
{
    if (1 | 1)
        printf("It's 1!\n");
        
    if (0 | 1)
        printf("It's 1!\n");
        
    if (0 | 0)
        printf("It's 0!\n");
    
    return (0);
}

 

두 Bit 중 하나라도 1이면 1을 반환하기 때문에 Bit끼리 더하기 할 때 많이 사용된다.

2진수가 0과 1로만 이루어져 있기 때문에 { 0 + 1 = 1 }이 성립한다.

^(XOR)

▼ 두 Bit가 반대되면 1을 반환한다.

#include <cstdio>

int main()
{
    if (1 ^ 1)
        printf("It's 0!\n");
        
    if (1 ^ 0)
        printf("It's 1!\n");
       
    return (0);
}

 

두 Bit가 반대되면 1을 반환하기 때문에 Bit끼리 빼기 할 때 많이 사용된다.

|(OR)와 마찬가지로 2진수의 특징 때문에 { 1 - 1 = 0 }이 성립한다.

!(NOT)

▼ 하나의 Bit를 부정한다.

#include <cstdio>

int main()
{
    if (!0)
        printf("It's 1!\n");
    
    return (0);
}

하나의 Bit를 부정한다.

0이면 1로 1이면 0이 되기 때문에 조건문에서 자주 사용된다.

비트 연산자 응용

비트 연산자를 익혀도 어떨 때 쓰는지 모른다면 말짱 도루묵이다.

이번에는 실제로 게임 개발에서 자주 쓰이는 비트 플래그(Bit Flag)를 통해서 실제로 사용해본다.

비트 플래그(Bit Flag)

비트 플래그는 어떻게 하면 조금이라도 더 메모리를 작게 쓸 수 있을까란 고민에서 나온 최적화 방법 중 하나다.

 

▼ 바라보는 관점을 바꾸는 비트 플래그 원리

C에서 가장 작은 변 수형인 Char(1Byte)를 기준으로 생각해보자.

1ByteBit8개 모인 것이니 관점에 따라서는 Bit형 길이 8인 배열로 생각해 볼 수도 있다.

즉, 하나의 Char에 단순히 0과 1만 가지는 값이라면 8개나 들어갈 수 있는 것이다!

 

게임에서 0과 1을 사용하는 것을 트리거 혹은 플래그라고 하는데

대표적으로 퀘스트 완료 여부, 아이템 옵션 값 같은 경우에 사용한다. 

 

말로는 이해가 돼도 손은 아닐 수도 있으니 직접 코드를 작성해보자.

 

▼ 비트 플래그 전체 코드

#include <cstdio>

// 플래그 설정
typedef enum _BitFlag {
    TUTORIAL = 1,
    CHAPTER_A = 2,
    ENDING = 4,
}BitFlag;

// 플래그 추가
void AddFlag(char* flagContainer, BitFlag flagType)
{
    *flagContainer |= flagType;
};

// 플래그 제거
void RemoveFlag(char* flagContainer, BitFlag flagType)
{
    *flagContainer ^= flagType;
};

// 플래그 초기화
void ClearFlag(char* flagContainer)
{
    *flagContainer = 0;
}

int main()
{
    char currentProcess = 0;
    
    AddFlag(&currentProcess, TUTORIAL);
    if (currentProcess == TUTORIAL)
        printf("CURRENT : TUTORIAL\n");
        
    AddFlag(&currentProcess, CHAPTER_A);
    if (currentProcess == (TUTORIAL || CHAPTER_A))
        printf("CURRENT : TUTORIAL, CHAPTER_A\n");
        
    RemoveFlag(&currentProcess, CHAPTER_A);
    if (currentProcess == TUTORIAL)
        printf("CURRENT : TUTORIAL\n");
    
    ClearAllFlag(&currentProcess);
    
    return (0);
}

좀 더 직관적으로 흐름을 이해하기 위해 조건문으로 일일이 확인하도록 했다.

이제 하나씩 차근차근 살펴보자.

 

열거형을 사용하면 관리하기 편하다.

// 플래그 설정
typedef enum _BitFlag {
    TUTORIAL = 1,
    CHAPTER_A = 2,
    ENDING = 4,
}BitFlag;

먼저 열거형(Enum)을 사용해서 게임 속에서 사용할 플래그 값들을 미리 정의한다.

이 과정이 없어도 비트 플래그를 사용하는 데에는 문제없지만 관리하는 측면에서는 훨씬 편리하다.

 

여기에는 특별한 규칙이 있는데 { 1, 2, 3, 4 }가 아니라 { 1, 2, 4, 8 } 처럼 2의 배수할당해야 한다.

비트 플래그가 연산을 하다 값이 겹쳐버리면 엉뚱한 비트 플래그가 발동할 수 있기 때문이다.

그래서 구조적으로 1을 가지는 요소가 단 하나이기 때문에 어떻게 연산해도 값들이 겹치지 않는다.

 

▼ 플래그 추가, 제거, 초기화 함수

// 플래그 추가
void AddFlag(char* flagContainer, BitFlag flagType)
{
    *flagContainer |= flagType;
};

// 플래그 제거
void RemoveFlag(char* flagContainer, BitFlag flagType)
{
    *flagContainer ^= flagType;
};

// 플래그 초기화
void ClearFlag(char* flagContainer)
{
    *flagContainer = 0;
}

플래그를 추가하는 AddFlag() 함수에서 |(OR)가 사용되었다.

 

*flagContainer의 Bit[8]에서 0인 비트들을 flagType이 가진 Bit[8]들과 비교해서

같은 자리에 1이 있다면 1로 반환한다.

 

조건문으로 표현하면 아래와 같다.

if (flagContainer[n] == 0 && flagType[n] == 1)
    return (1);

 

2진수에는 0과 1밖에 없으니 결과적으로 { 0 + 1 = 1 } 과 같은 일을 한 셈이다.

 

반면 플래그를 삭제하는 RemoveFlag() 함수에서 ^(XOR)가 사용되었다.

 

^(XOR) 연산이 원래 두 Bit 가 값이 다르면 1을 반환하는 것이

반대로 두 Bit가 같다면 0을 반환하는 것과 같다는 사실을 이용한다.

 

조건문으로 표현하면 아래와 같다.

if (flagContainer[n] == flagType[n] )
    return (0);

마찬가지로 2진수에서는 결과적으로 { 1 - 1 = 0 } 과 같은 일을 한 셈이다.

 

비트 플래그의 초기값은 그냥 0이기 때문에 ClearFlag()에서 보면 Char 변수에 그냥 0을 넣어주고 있다.

'<CPP> > BASIC' 카테고리의 다른 글

19. 클라스(Class)  (0) 2021.01.22
18. 시프트 연산자(Shift Operator)  (0) 2021.01.20
16. 사용자 정의 자료형  (0) 2021.01.19
15. 동적 할당과 배열의 차이  (0) 2021.01.18
14. 할당, 해제  (0) 2021.01.15

댓글