이번 편은 비트 수준으로 데이터를 다뤄보는 이야기에요. 총 2부로 구성될 예정이니, 잘 따라와주세요 😉
말로만 들으면 어려워 보이는 이야기에요. 마치 앤트맨이 양자 영역에 들어가듯, 데이터를 비트 수준으로 다루는 것은 프로그래머에게 있어선 가장 작은 단위로 데이터를 다루는 것과 같아요. 우리도 이제부터 앤트맨처럼 버튼을 꾹 누르고 가장 작은 비트의 세계로 뛰어들어보려 해요.
C언어에서 비트를 어떻게 다루는지 한번 알아볼게요. C언어에서, 변수 등 모든 내용은 메모리 상에 0과 1의 이진수로 저장돼요. 이 때, 이진수 값의 각 자리를 비트 (bit)라고 말해요. 이 비트를 8개 묶어서, 바이트 (byte)라고 말해요.
프로그램이 메모리에서 값을 한번에 읽어들이는 단위를 워드라고 해요.
비트열의 예시를 들면 아래와 같아요.
(사실 버튼을 누른다고 작아지거나 그러진 않아요 😉)
비트 수준으로 값을 다룰 때, 비트 단위 연산자를 사용해요. 정수형 수식 (char, short, int, long, long long 등)에 사용할 수 있는 연산자이며, 비트 수준으로 다루다보니 시스템에 종속적이에요.
비트 연산자는 6가지 있는데, 이를 표로 정리해보면 아래와 같아요.
연산자 종류 | 연산자 이름 | 연산자 |
---|---|---|
논리 연산자 | (단항) 비트단위 보수 | ~ |
비트단위 논리곱 | & | |
비트단위 배타적 논리합 | ^ | |
비트단위 논리합 | | | |
이동 연산자 | 왼쪽 이동 | << |
오른쪽 이동 | >> |
이제 각 연산자들을 하나씩 알아볼게요.
~
연산자는 단항 연산자로, 뒤에 오는 값의 1의 보수 (비트 단위 보수)를 계산해요. 피 연산자의 각 비트를 0은 1로, 1은 0으로 바꿔줘요.
int a = 0b00001111;
int b = ~a;
// b = 0b11110000;
비트 단위 이진 논리 연산자는 3종류가 있어요.
두 피연산자는 대응되는 비트끼리 (같은 자리의 비트끼리) 연산돼요. 각 연산자별 진리표는 아래와 같아요.
a | b | a & b | a | b | a ^ b |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 1 | 1 |
0 | 1 | 0 | 1 | 1 |
1 | 1 | 1 | 1 | 0 |
예제 코드와 함께 알아볼게요.
void binary_print(int); // 이진수 값으로 출력해주는 함수에요.
int a = 0b0011;
int b = 0b1100;
int c = 0b0101;
binary_print(a & b); // 0000
binary_print(a | b); // 1111
binary_print(a ^ b); // 1111
binary_print(a & c); // 0001
binary_print(b & c); // 0100
binary_print(a | c); // 0111
binary_print(b | c); // 1101
binary_print(a ^ c); // 0110
binary_print(b ^ c); // 1001
마스킹 연산이란, 이름 그대로 비트열의 특정 부분을 0으로 가리는것을 말해요. &
연산자를 사용해, 주어진 비트열의 특정 비트만 0으로 만들고 나머지 부분을 판단할 수 있어요.
아래 코드는 반복문을 돌며 i가 홀수인지 짝수인지를 출력하는 코드에요.
int i, mask = 1; // mask = 0001
for (i = 0; i < 10; i++)
printf("%d : %s\n", i, i & mask ? "홀수" : "짝수");
위 코드를 해석해볼게요.
우선, 1, 2, 3, 4를 비트열로 나타내볼게요.
홀수의 경우, 마지막 비트가 항상 1임을 알 수 있어요. 이를 이용해서, mask 변수와 &
연산자를 활용해 마지막 비트의 값을 얻어내, 이 비트가 1인지 0인지에 따라 "홀수"와 "짝수"를 출력하고 있어요.
다음 예제는 int형 변수 v의 값을 mask 변수를 활용해 마스킹하는 코드에요.
int v, mask = 255; // mask = 0000 0000 1111 1111
int masked = v & 255;
위 코드에서, 몇개 비트가 마스킹될까요?
✅정답은..?
8개 비트가 마스킹돼요! 255를 비트열로 나타내보면
00000000 11111111 이에요.&
연산자를 사용하면 1인 부분의 비트열만 추려질테니, 결과적으로 하위 8개 비트의 값을 얻을 수 있어요.
이동 연산자는 지정된 값의 비트열을 이동시켜요. 이 때, 이동 연산자의 두 피연산자는 모두 정수 수식이어야 해요.
이동 연산자는 2종류 존재해요.
<<
>>
수식 1 << 수식 2
수식 1의 비트 표현을 수식 2만큼 왼쪽으로 이동해요. 이 때, 왼쪽으로 이동해 빈 공간은 0으로 채워져요. 또, 수식 1의 자료형의 표현 한계를 벗어나는 비트열들은 사라져요. 비트열을 왼쪽으로 한칸 밀기 때문에, 2의 거듭제곱 효과를 볼 수 있어요.
아래 예제를 보고 이해해볼게요.
int c = 15;
// c = 00000000 00000000 00000000 00001111
이 때, 왼쪽으로 30비트 민 경우는 2개 비트가 손실된 것을 확인할 수 있어요.
수식 1 >> 수식 2
수식 1의 비트열을 수식 2 만큼 오른쪽으로 이동해요. 이 때, 수식 1이 unsigned형이면 빈 상위 비트를 0으로 채워요. 반면, 수식 1이 signed 형일 경우는 시스템에 따라 동작이 달라요. 상위 비트를 0으로 채울수도 있고, 1로 채울수도 있어요.
아래 예제를 보고 이해해볼게요.
unsigned int c = 0xf0000000; // 4026531840
// c = 11110000 00000000 00000000 00000000
왼쪽으로 밀 때와 마찬가지로, 표현 범위를 벗어나 오른쪽으로 밀려난 값들은 사라져요. 왼쪽으로 30칸 밀 때 왼쪽 끝에 2개 비트가 손실된 것 처럼, 오른쪽으로 31칸 밀 때 오른쪽 끝 3개 비트가 손실된 것을 확인할 수 있어요.
signed형에 대해 오른쪽 이동 연산자를 사용할 경우는 아래 2가지 중 1개의 결과를 얻을 수 있어요.
signed int c = 0xf0000000; // -268435456
// c = 11110000 00000000 00000000 00000000
사칙 연산자들 (+, -, *, /) 들처럼, 비트 단위 연산자들도 복합 배정 연산자를 가져요.
&=
|=
^=
<<=
>>=
예를 들면 다음과 같아요.
int a = 15;
a &= 255; // a = a & 255
아까 이진수로 출력하는 함수의 구현부를 적지 않았었는데, 이번 예제로 알아볼게요.
#include <stdio.h>
void binary_print(int a)
{
int n = sizeof(int) * 8; // 전체 비트 수
int mask = 1 << (n-1); // n개 비트 중 가장 최상위 비트만 1인 마스크에요.
// 2^(n-1) 자리의 비트부터 2^0까지 n개 비트를 반복해요
for (int i = 1; i <= n; i++)
{
putchar((a & mask) ? '1' : '0');
// 이진수로 출력할 숫자값을 왼쪽으로 한칸씩 밀어 저장해요.
// 마스크 값이 n개 비트 중 가장 최상위 비트에만 있기 때문이에요.
a <<= 1;
if (i % 8 == 0 && i < n)
putchar(' '); // 가독성을 위해, 8개 비트마다 끊어서 표기해요.
}
putchar('\n'); // 개행문자를 마지막에 붙여줘요.
}
int main(void)
{
printf("2진수로 나타낼 정수를 입력하세요.\n> ");
int i;
scanf("%d", &i);
binary_print(i);
return 0;
}
🖥️실행결과
2진수로 나타낼 정수를 입력하세요. 255 00000000 00000000 00000000 11111111 2진수로 나타낼 정수를 입력하세요. 1 00000000 00000000 00000000 00000001 2진수로 나타낼 정수를 입력하세요. -1 11111111 11111111 11111111 11111111
배운 내용들을 정리해보고 있어요. 잘못 기재된 내용이 있다면, 댓글로 지적해주시면 수정할게요.