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
)이 된다.
- 마찬가지로
0000 1111
(15
)을 오른쪽으로2
번 이동시키면 맨 뒤의11
은 사라져서0000 0011
(3
)이 된다.
- 즉, 비트에서 첫째 자리나 마지막 자리를 넘어서는 비트는 그대로 사라진다.
최상위 비트, 최하위 비트
- 비트에서 첫 번째 비트를 최상위 비트, 마지막 비트를 최하위 비트라고 부른다.
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
)가 나왔다.
- 부호 있는 자료형의 첫 번째 비트는 부호 비트라고 하는데, 이 비트가
1
이면 음수,0
이면 양수이다.
- 부호 있는 자료형에 저장된
1000 0011
은 첫 번째 비트가1
이므로 음수이고 10진수로는-125
가 된다. - 이 비트들을 오른쪽으로
5
번 이동시키면 모자라는 공간은 모두 부호 비트의 값으로 채워지기 때문에1111 1100
(-4
)가 된다. - 하지만 부호 없는 자료형은 비트를 오른쪽으로 이동해도 모자라는 공간은 모두
0
으로 채워진다. - 즉, 비트 연산자는 부호 있는 자료형과 부호 없는 자료형이 다르게 동작한다.
- 그러면 부호 있는 자료형에서 부호 비트가
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
)이 된다.
- 이번에는 부호 있는 자료형에서
<<
연산을 해보자.
#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
)이 되고, 양수였던 수가 음수가 되어버린다.
- 이미 첫 번째 비트가
1
인 음수1111 0001
(-15
)는 왼쪽으로2
번 이동시켜서 부호 비트를 덮어쓰더라도 부호 비트는 바뀌지 않으므로 계속 음수1100 0100
(-60
)이 된다.
- 비트를 왼쪽으로 좀 더 이동시켜서
0
이 부호 비트를 덮어쓰게 만들면 음수였던 수는 양수가 된다. - 즉, 음수인
1111 0001
(-15
)를 왼쪽으로4
번 이동시키면1
은 모두 사라지고, 부호 비트에0
이 와서 양수0001 0000
(16
)이 된다.
- 이미 첫 번째 비트가
0
인 양수0111 0001
(113
)을 왼쪽으로4
번 이동시키면 앞의1
은 모두 사라지고, 부호 비트에0
이 오므로 계속 양수0001 0000
(16
)이 된다.
6. 비트 연산자로 플래그 처리하기
- 플래그는 깃발에서 유래한 용어이다.
- 보통 깃발을 위로 올리면
on
, 아래로 내리면off
를 뜻한다. - 이걸 정수의 비트에 활용하는 건데 비트가
1
이면on
,0
이면off
를 나타낸다.
- 다음과 같이
8
비트(1
바이트) 크기의 자료형은 비트가8
개 들어가므로8
가지 상태를 저장할 수 있다. - 여기서는 두 번째 비트와 여덟 번째 비트가 켜진 상태이다.
- 그렇다면
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 | 1
과1 | 1
은1
이므로flag
의 비트가 꺼져있으면 비트를 켜고, 켜져 있으면 그대로 유지한다.
- 플래그의 특정 비트가 켜져 있는지 검사하려면
&
연산자를 사용하면 된다.
if (flag & 4) // & 연산자로 0000 0100 비트가 켜져 있는지 확인
printf("0000 0100은 켜져 있음\n");
else
printf("0000 0100은 꺼져 있음\n");
&
연산자는 두 비트가 모두1
이어야1
이다.- 따라서
flag
에 저장된0000 0111
과 마스크 값0000 0100
(4
)를&
로 연산하면 여섯 번째 비트가1
이 된다. - 연산 결과가 마스크 값이 나오면 비트가 켜져 있는 것이고,
0
이 나오면 꺼져 있는 것이다.
- 이번에는 플래그의 비트를 끄는 방법이다.
#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은 켜져 있음
- 마스크 값을
~
연산자로 비트를 뒤집은 뒤&=
연산자를 사용하여 특정 비트를 끈다.
- 먼저 마스크 값
2
의 비트를 뒤집는다.
- 이렇게 하면 끄고자 하는 비트 이외의 값은 모두
1
이 된다. - 그리고
flag
에 마스크 값의 비트를 뒤집은 값으로&
연산하면 비트를 끌 수 있다.
- 즉,
1111 1101
에서1
은flag
의 원래 있던 비트 값을 유지한다. - 비트 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은 켜져 있음
^=
연산자와 마스크를 사용하여 특정 비트가 켜져 있으면 끄고, 꺼져 있으면 켠다.
- 플래그의 비트를 토글하는 동작은 XOR 연산의 특성을 활용한 것이다.
- 두 비트가 다르면
1
, 같으면0
이다. - 따라서
flag
의 비트가1
이라면 마스크의1
과 같으므로0
이 되고, 0이라면 마스크의1
과 다르므로1
이 되는 원리이다.
- 여기서는
0000 0111
에서 일곱 번째 비트를 토글하고, 다섯 번째 비트를 토글했으므로0000 1101
이 된다.