본문 바로가기
C++/기초

C++ 기초 : 비트 연산자 (2)

글: 시플마 2024. 2. 16.

비트 연산을 왜 할까요?

 

예를 들어 

시뮬레이션 게임이 있다고 합시다.

 

캐릭터가 가질 수 있는 여러 상태가 있겠죠?

우리는 변수 하나로 모든 상태를 표현할 수 있습니다.

unsigned int iStatus = 0;

 

위와 같이 캐릭터 상태를 담을

변수 iStatus를 선언해 보겠습니다. 

 

int형이기 때문에 4byte, 즉 32bit입니다.

이는 32개의 칸으로 이루어졌다고 볼 수 있죠.

 

각각 칸마다 어떤 상태를 표현할지 정하고,

해당 칸이 0이면 해당 상태가 아니고 1이면 해당 상태라고

표현할 수 있는 것입니다.

32가지 상태를 표현할 수 있겠군요.

 

아래와 같은 코드가 있습니다.

#define HUNGRY 1
#define THIRSTY 2

int main() {
	unsigned int iStatus = 0;
	iStatus = HUNGRY;

	return 0;
}

 

캐릭터 상태를 담당하는 변수 iStatus에 HUNGRY를 대입했습니다.

1이 담기겠죠. 메모리 상태는 아래와 같을 것입니다.

0     ...   0 0 1

 

 

이번에는 캐릭터 상태를 담당하는 변수 iStatus에 HUNGRY가 아닌,

THIRSTY를 대입하겠습니다.

2가 담깁니다. 이를 이진수로 표현하여 메모리에 담기므로

메모리 상태는 아래와 같을 것입니다.

#define HUNGRY 1
#define THIRSTY 2

int main() {
	unsigned int iStatus = 0;
	iStatus = THIRSTY;

	return 0;
}
0     ...   0 1 0

 

 

이처럼 첫 번째 칸에 1이 들어와 있으면

캐릭터의 상태를 HUNGRY(배고픈)로 보고 

두 번째 칸에 1이 들어와 있으면 

캐릭터의 상태를 THIRSTY(목마른)로 보는 것이죠.

 

 


 

 

 

하지만 

캐릭터의 상태가 항상 한 가지일 수는 없습니다.

배고프면서 목이 마를 수도 있거든요.

 

그때마다 새로 대입하면 기존 값이 사라지기 때문에 

이때 비트 연산을 통해 상태를 추가해 주는 겁니다.

 

캐릭터가 배고픈 상태에서 목마른 상태를 추가하려면

어떤 연산을 사용해야 할까요?

 

아래와 같이 비트 합( | ) 연산을 하면 됩니다.

#define HUNGRY 1
#define THIRSTY 2

int main() {
	unsigned int iStatus = 0;
	iStatus = HUNGRY;
	iStatus |= THIRSTY;

	return 0;
}

 

 

캐릭터 상태를 나타내는 변수 iStatus에

HUNGRY가 대입된 모습.

0     ...   0 0 1

 

여기에 비트 합 연산을 통해 

THIRSTY 상태를 추가하겠습니다.

 

|

 

0     ...   0 1 0

 

=

 

0     ...   0 1 1

 

비트 합 연산은 둘 중 하나라도 1이면

1이라는 결과를 냅니다.

이 말은 기존 상태를 그대로 유지할 수 있고

그 상태로 값을 추가할 수 있다는 것입니다.

 

 

 


 

 

캐릭터가 배고픈지 배고프지 않은지 체크해서

배고프면 체력을 닳게 하고 싶습니다.

이때는 비트 곱( & ) 연산을 사용하면 됩니다.

#define HUNGRY 1
#define THIRSTY 2

int main() {
	unsigned int iStatus = 0;
	iStatus |= HUNGRY;
	
        if(iStatus & HUNGRY)
        {
            //체력을 닳게 하는 코드
        }

	return 0;
}

 

위 코드를 보면

iStatus에 HUNGRY가 들어와 있습니다.

그 상태에서 if문을 통해 상태를 체크합니다.

iStatus와 HUNGRY를 비트 곱 연산을 하여

참이면 체력을 닳게 하는 코드를 실행하려고 하죠.

 

아래는 캐릭터 상태를 나타내는 변수 iStatus에

HUNGRY가 대입된 모습입니다.

0     ...   0 0 1

 

여기에 HUNGRY 값과 비트 곱 연산을 해봅니다.

 

&

0     ...   0 0 1

 

=

0     ...   0 0 1

 

비트 곱 연산은 

비트를 비교하여 둘 다 1이어야만 1이 됩니다.

 

다른 비트들은 0이므로 0이 나옵니다.

다른 비트에 1이 들어와 있어도

HUNGRY 값을 비트 곱 연산하는 것이기 때문에

첫 번째 칸을 제외하면

무조건 0이 나올 수밖에 없습니다.

 

iStatus의 첫 번째 값도 1이고

HUNGRY를 나타내는 첫 번째 비트도

1이므로 결과적으로 1이 나옵니다.

 

해당 캐릭터는 배고픈 상태임을 나타내죠.

if문 조건식을 계산한 결과

1이 나왔습니다.

컴퓨터에서 0이 아닌 다른 수는 참이므로 

참으로 인식하고 if문 중괄호 안에 코드를 수행하겠죠.

 

 

 


 

 

 

그렇다면

캐릭터가 배고프고 목마른 상태에서

음식을 먹여 배고픈 상태를 해결하는 코드를 만들고 싶습니다.

즉, HUNGRY에 해당하는 비트 값만 0으로 만들고 싶습니다.

 

먼저 해 볼 수 있는 건 비트 XOR( ^ ) 연산입니다.

 

현재 iStatus에 HUNGRY와 THIRSTY가 대입되어 있는 상태입니다.

0     ...   0 1 1

 

여기서 비트 XOR 연산을 통해 HUNGRY 상태를 없애려고 합니다.

 

^

0     ...   0 0 1

 

=

0     ...   0 1 0

 

THIRSTY는 유지한 채 

HUNGRY만 제거하는데 성공했습니다.

 

 

그러나 해결한 듯 보이지만 

캐릭터가 배고프지 않은 상태에서 

비트 XOR 연산을 통해 

상태를 제거하려고 시도하면

문제가 발생합니다.

 

현재 iStatus에 THIRSTY만 대입되어 있는 상태입니다.

0     ...   0 1 0

 

여기서 비트 XOR 연산을 통해

HUNGRY 상태인지 아닌지 체크하려 합니다.

 

^

0     ...   0 0 1

 

=

0     ...   0 1 1

 

캐릭터가 배고프지 않은 상태에서

배고픈 상태를 제거하려고 시도하였더니

배고픈 상태로 바뀌었습니다.

 

비트 XOR 연산은

값이 다르면 1이 되기 때문이죠.

 

이 문제를 해결하기 위해서

우선 캐릭터가 배고픈지

배고프지 않은지 확인부터 한 후,

배고픈 상태면 비트 XOR 연산을 통해 값을 제거하고

배고프지 않은 상태면 수행하지 않는 방향으로 코드를 작성해야 합니다.

 

 


 

 

 

위에서 설명한 복잡한 방법보다

더 좋은 방법이 있습니다.

제거하려는 상태를 먼저 반전시키고 

캐릭터 상태와 비트 곱 연산을 하는 것입니다.

 

코드로 작성하면 아래와 같겠죠.

#define HUNGRY 1
#define THIRSTY 2

int main() {
	unsigned int iStatus = 0;
	iStatus = HUNGRY;
	iStatus |= THIRSTY;
    
   	iStatus &= ~HUNGRY;

	return 0;
}

 

 

HUNGRY와 THIRSTY가 대입된

변수 iStatus의 메모리 상태는 아래와 같습니다.

0     ...   0 1 1

 

여기서 HUNGRY 값만 제거하기 위해서

먼저 HUNGRY의 비트를 반전시켜야 합니다.

 

 

~

0     ...   0 0 1

 

=

1     ...   1 1 0

 

 

iStatus의 값과

이 HUNGRY를 반전시킨 값과

비트 곱 연산하여 합니다.

0     ...   0 1 1

 

&

1     ...   1 1 0

 

=

0     ...   0 1 0

 

계산 결과,

iStatus에서 목마른 상태는 그대로 유지한 채

배고픈 상태만 제거된 것을 확인할 수 있습니다.

 

 

 

이 방식이라면 HUNGRY 값이 0일 때도 

정상 작동합니다. 확인해 보겠습니다.

 

현재 iStatus에 THIRSTY만 대입되어 있는 상태입니다.

0     ...   0 1 0

 

여기서 반전시킨 HUNGRY 값과 

비트 곱 연산을 합니다.

 

&

1     ...   1 1 0

 

=

0     ...   0 1 0

 

 

비트 XOR 연산과 다르게

기존 상태 값이 0이어도 

정상 작동하는 것을 확인할 수 있습니다.

 

 

 


 

 

 

지금까지는 32비트 중 두 개의 비트만 사용하여

배고픔과 목마름 상태를 표현하였습니다.

더 많은 비트를 사용하여 다양한 상태를 표현하기 위해

매크로를 추가하여 정의하겠습니다.

#define HUNGRY  1
#define THIRSTY 2
#define FIRE 	4
#define POISON 	8
#define COLD 	16

 

규칙이 보이죠?

한 칸을 더 사용할 때마다

기존 값에서 두 배한 값으로 정의하고 있습니다.

 

아래 그림을 통해 이유를 살펴보겠습니다.

    ... 2^4번째 칸
(COLD)
2^3번째
(POISON)
2^2번째
(FIRE)
2^1번째
(THIRSTY)
2^0번째
(HUNGRY)

 

메모리에서 0번째 칸을 HUNGRY 상태를 나타내는 비트로 사용하려면

2의 0제곱인 1로 정의하면 됩니다.

 

마찬가지로 

메모리에서 4번째 칸을 COLD 상태를 나타내는 비트로 사용하려면

2의 4제곱인 16으로 정의하면 되는 것이죠.

 

 

 

지금까지 #define 지시문을 통해

매크로를 정의할 때

숫자를 10진수로 표현하였습니다.

#define HUNGRY  1
#define THIRSTY 2
#define FIRE 	4
#define POISON 	8
#define COLD 	16

 

 

이것을 16진수로 표현하는 경우도 있습니다.

C++에서 16진수를 표현하려면 숫자 앞에 '0x'를 붙이면 됩니다.

#define HUNGRY  0x1
#define THIRSTY 0x2
#define FIRE 	0x4
#define POISON 	0x8
#define COLD 	0x10

 

여기서 특이한 점은

10진수에서 16인 수를 16진수로 표현하면 10이 됩니다.

이유는 16진수는 수를 0 ~ 9, A ~ F로 표현하기 때문입니다.

 

나열하면
0 1 2 3 4 5 6 7 8 9 A(10) B(11) C(12) D(13) E(14) F(15)

위와 같은 형태라는 것이죠.

 

10진수에서는 수를 0 ~ 9로 표현하기에

숫자 10을 표현하기 위해서는 09에서 자릿수를 올려 표현합니다.

 

16진수도 마찬가지입니다.

숫자 16을 표현하기 위해서는 자릿수를 올려 0F에서

자릿수를 올려 표현합니다.

 

 

매크로를 더 정의해 볼까요?

#define HUNGRY  0x001
#define THIRSTY 0x002
#define FIRE 	0x004
#define POISON 	0x008

#define COLD0 	0x010
#define COLD1 	0x020
#define COLD2 	0x040
#define COLD3 	0x080

#define COLD4 	0x100
#define COLD5 	0x200
#define COLD6 	0x400
#define COLD7 	0x800

 

COLD0이 2의 4제곱, 즉 16이라는 수로 정의했습니다. 

 

그 다음 칸을

COLD1 상태를 표현하는 비트로 사용하려면

2의 5제곱으로 정의하면 되겠죠.

10진수로 표현했을 때 32에 해당하는 수이죠.

 

32는 십진수로 16 + 16입니다.

이를 16진수로 표현하면

10 + 10이므로 20으로 표현할 수 있습니다.

 

다음 COLD2는 2의 6제곱, 십진수로 64로 정의되겠죠?

64는 16이 4개입니다.

즉 16진수로 40으로 표현할 수 있습니다.

 

쭉 넘어가서 COLD4를 살펴보겠습니다.

2의 8제곱인 256으로 정의될 것입니다.

256은 16이 16개이죠.

 

10진수에서 10이 한 개 있으면 10으로 표현하죠?

10이 10개 있으면 100으로 표현합니다.

 

16진수에서는 16이 한 개 있으면 16이며

16이 16개 있으면 100으로 표현합니다.

 

따라서 2의 8제곱으로 정의되는 COLD4의 값은

256이며 이를 16진수로 표현하면 100입니다.

 

 

 

원리는 꽤 복잡해 보이지만

16진수로 표현할 때 장점은

4개로 묶어 자리를 한눈에 알아보기 편하다는 점입니다.

 

HUNGRY ~ POISON까지는 0 ~ 3번째 비트에 할당하고

COLD0 ~ COLD3까지는 4 ~ 7번째 비트에 할당,

COLD4 ~ COLD7까지는 5 ~ 8번째 비트에 할당함을 알 수 있습니다.

 

16진수에서 비트를 표현할 때

1, 2, 4, 8이 반복되고 8이 나온 다음에는

자릿수를 올리면 됩니다. 이후 다시 1부터 8이 반복되는 규칙이 있습니다.

 

만약 위 매크로 정의에서 COLD8이 추가된다면 0x1000으로 정의하면 되겠죠.

 

 

 

 

 

 

강의 출처 : https://www.youtube.com/watch?v=PFc4g8mxOiI&list=PL4SIC1d_ab-aOxWPucn31NHkQvNPHK1D1&pp=iAQB