C언어) 비트의 세계 - 비트 단위 연산자

Lapis0875·2022년 10월 24일
0

c언어

목록 보기
12/21
post-thumbnail

😎비트수준으로 접근해보기

이번 편은 비트 수준으로 데이터를 다뤄보는 이야기에요. 총 2부로 구성될 예정이니, 잘 따라와주세요 😉

😥비트 수준? 그게 무슨 말이에요;;

말로만 들으면 어려워 보이는 이야기에요. 마치 앤트맨이 양자 영역에 들어가듯, 데이터를 비트 수준으로 다루는 것은 프로그래머에게 있어선 가장 작은 단위로 데이터를 다루는 것과 같아요. 우리도 이제부터 앤트맨처럼 버튼을 꾹 누르고 가장 작은 비트의 세계로 뛰어들어보려 해요.

버튼을 누르기 전에...

C언어에서 비트를 어떻게 다루는지 한번 알아볼게요. C언어에서, 변수 등 모든 내용은 메모리 상에 0과 1의 이진수로 저장돼요. 이 때, 이진수 값의 각 자리를 비트 (bit)라고 말해요. 이 비트를 8개 묶어서, 바이트 (byte)라고 말해요.
프로그램이 메모리에서 값을 한번에 읽어들이는 단위를 워드라고 해요.

비트열의 예시를 들면 아래와 같아요.

  • 0101 1101
  • 931093_{10} = (2^6 + 2^4 + 2^3 + 2^2 + 2^0)
  • 5D165D_{16} = (16진수로, 0101 = 5, 1101 = D)

🕹️이제 버튼을 눌러볼게요!

(사실 버튼을 누른다고 작아지거나 그러진 않아요 😉)

비트 단위 연산자

비트 수준으로 값을 다룰 때, 비트 단위 연산자를 사용해요. 정수형 수식 (char, short, int, long, long long 등)에 사용할 수 있는 연산자이며, 비트 수준으로 다루다보니 시스템에 종속적이에요.

비트 연산자는 6가지 있는데, 이를 표로 정리해보면 아래와 같아요.

연산자 종류연산자 이름연산자
논리 연산자(단항) 비트단위 보수~
비트단위 논리곱&
비트단위 배타적 논리합^
비트단위 논리합|
이동 연산자왼쪽 이동<<
오른쪽 이동>>

이제 각 연산자들을 하나씩 알아볼게요.

🔀비트 단위 보수 연산자 ~

~ 연산자는 단항 연산자로, 뒤에 오는 값의 1의 보수 (비트 단위 보수)를 계산해요. 피 연산자의 각 비트를 0은 1로, 1은 0으로 바꿔줘요.

int a = 0b00001111;
int b = ~a;
// b = 0b11110000;

📱비트 단위 이진 논리 연산자

비트 단위 이진 논리 연산자는 3종류가 있어요.

  • 논리곱 (AND) : &
  • 논리합 (OR) : |
  • 배타적 논리합 (XOR) : ^

두 피연산자는 대응되는 비트끼리 (같은 자리의 비트끼리) 연산돼요. 각 연산자별 진리표는 아래와 같아요.

aba & ba | ba ^ b
00000
10011
01011
11110

예제 코드와 함께 알아볼게요.

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 : 0001
  • 2 : 0010
  • 3 : 0011
  • 4 : 0100

홀수의 경우, 마지막 비트가 항상 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
  • c << 1 : 00000000 00000000 00000000 00011110 = 301030_{10}
  • c << 8 : 00000000 00000000 00001111 00000000 = 3840103840_{10}
  • c << 30 : 11000000 00000000 00000000 00000000 = 107374182410-1073741824_{10}

이 때, 왼쪽으로 30비트 민 경우는 2개 비트가 손실된 것을 확인할 수 있어요.

오른쪽 이동 연산자

수식 1 >> 수식 2
수식 1의 비트열을 수식 2 만큼 오른쪽으로 이동해요. 이 때, 수식 1이 unsigned형이면 빈 상위 비트를 0으로 채워요. 반면, 수식 1이 signed 형일 경우는 시스템에 따라 동작이 달라요. 상위 비트를 0으로 채울수도 있고, 1로 채울수도 있어요.

아래 예제를 보고 이해해볼게요.

unsigned int c = 0xf0000000;	// 4026531840
// c = 11110000 00000000 00000000 00000000
  • c >> 2 : 00111100 00000000 00000000 00000000 = 1006632960101006632960_{10}
  • c >> 8 : 00000000 11110000 00000000 00000000 = 157286401015728640_{10}
  • c >> 31 : 00000000 00000000 00000000 00000001 = 1101_{10}

왼쪽으로 밀 때와 마찬가지로, 표현 범위를 벗어나 오른쪽으로 밀려난 값들은 사라져요. 왼쪽으로 30칸 밀 때 왼쪽 끝에 2개 비트가 손실된 것 처럼, 오른쪽으로 31칸 밀 때 오른쪽 끝 3개 비트가 손실된 것을 확인할 수 있어요.

signed형에 대해 오른쪽 이동 연산자를 사용할 경우는 아래 2가지 중 1개의 결과를 얻을 수 있어요.

signed int c = 0xf0000000;	// -268435456
// c = 11110000 00000000 00000000 00000000
  • c >> 8 : 11111111 11110000 00000000 00000000
  • c >> 8 : 00000000 11110000 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

배운 내용들을 정리해보고 있어요. 잘못 기재된 내용이 있다면, 댓글로 지적해주시면 수정할게요.

profile
새내기 대학생 개발자에요 :D

0개의 댓글