C 언어 | C 언어 입문 | 비트 처리

논리적, 논리화, 비트 시프트 등을 사용하여 정수를 비트 단위로 조작하는 방법을 설명한다.

비트 연산

산술 연산는 수학적인 계산을 수행하는데 적합하지만, 2진수를 제어하려면 적합하다고는 말할 수 없다. 즉, 비트 단위로 계산을 할 경우, 산술 연산는 적합하지 않다. 예를 들어, 32비트의 정수형 변수에서 상위 8비트를 얻고하고자 하는 경우, 산술 연산이 아니라 비트 단위의 계산이 필요하다. 그래서 이러한 비트 제어를 할 경우는 비트 논리 연산을 한다.

표1 - 비트 연산자

연산자 의미
& 비트 논리곱
| 비트 논리합
^ 비트 배타적 논리합
~ 1의 보수
&= 비트 AND 대입
|= 비트 OR 대입
^= 비트 배타적 OR 대입

표1의 연산자를 비트 연산자라고 한다. 어떤지, 논리곱라든지 논리합든가, 어려울 것 같은 말이 나왔지만 복잡하게 생각하지 말자. 논리 연산은 2진수의 0과 1을 스위치의 ON과 OFF처럼 생각하고, 0의 상태를 거짓(FALSE), 1의 상태를 참(TRUE)이라고 생각할 수 있다.

우선는 논리곱부터 사용해 보자. 논리곱(AND)과 비교하는 두 비트가 양쪽 모두 1이면 참, 그렇지 않으면 거짓을 산출한다. 이 관계는 표2와 같이 된다.

표2 - 논리곱 관계

A B A & B
0 0 0
0 1 0
1 0 0
1 1 1

이러한 논리곱의 성질을 이용하면, 특정 비트만을 추출하는 것이 가능하다. 예를 들어 0101 1100는 2진수가 있는 경우, 하위 4비트의 값을 얻고 싶다면 0000 1111과의 논리곱을 구한다.

표1 - 논리곱에 의한 비트 추출

  0101 1100
& 0000 1111
------------
  0000 1100

이러한 계산을 시키면, 마스크 비트의 상위 4비트는 0이므로 결과는 항상 0이 되고, 하위 4비트는 모두가 1이므로, 비교하는 비트 중에 1부분만 1로 추출된다. 이 성질은 RGB 형식의 색상 정보를 나타내는 32비트 값에서 붉은 요소만을 추출하고 싶은 경우에 적용된다. 32비트 중 16 ~ 23 비트가 붉은 요소를 나타내는 경우, 이에 0xFF0000과의 논리곱을 구하여 붉은 요소의 값만을 얻을 수 있다.

코드1

#include <stdio.h>

int main() {
 char ch5 = '5';
 printf("ch5:%%c = %c , %%X = %X , & = %d\n" , ch5 , ch5 , ch5 & 0x0F);
 return 0;
}

이 프로그램은 ASCII 문자의 숫자와 실제의 수치의 관계를 알 수 있다. 문자 상수로써의 숫자(예를 들어 ‘5’)와 정수(예 : 5)는 전혀 다른 것이다. 실제로 문자 상수 ‘5’를 숫자로 출력하면 전혀 관계없는 값임을 알 수 있다. 그러나, ASCII 문자는 숫자도 0 ~ 9까지 존재하고, 그 규칙을 아는 것으로 문자 상수를 실제 숫자로 변환할 수 있다.

사실는 ASCII 문자의 숫자는 16진수 0x30 ~ 0x39까지 매핑되어 있다. 즉, 문자 상수 ‘0’은 ASCII 코드로 0x30이며, 반대로 생각을 하면 ASCII 코드로 0x37은 문자 상수 ‘7’이라고 생각할 수 있다. 이 규칙성을 살려, 문자 상수의 상위 4비트 값을 제거하면 순수한 숫자로 변환할 수 있다는 것이다. 코드1은 바로 이것을 ch5 & 0x0F으로 실행하고 있다. ASCII 코드의 숫자에 대해서 0x0F와의 논리곱을 구하여 숫자로 변환할 수 있다.

다음으로, 논리합(OR)인데, 이것은 비트 중에 하나가 1이면 참이라는 결과를 내고 있다. 모두 0으로 비교하면 원래 값이 모두 1로 비교하면 모든 비트가 1이 된다.

표3 - 논리합 관계

A B A | B
0 0 0
0 1 1
1 0 1
1 1 1

이 성질을 잘 이용하면 비트 플래그의 조합을 만들 수 있다. 또, 이전과는 반대로 숫자부터 ASCII 코드의 숫자로 변환하는 처리를 할 수 있다.

코드2

#include <stdio.h>

int main() {
  int iVar = 5;
 printf("iVar:%%d = %d\niVar | 0x30:%%c = %c\n" , iVar , iVar | 0x30);
 return 0;
}

이 프로그램은 숫자형 변수 iVar에 저장되어 있는 값을 ASCII 문자의 숫자로 변환한다. 다만, iVar 값은 1 자리이어야 한다.

배타적 논리합(XOR)는 두 값이 같으면 거짓이 되는 논리 연산으로 한쪽이 1이고 한쪽이 0일 때 참이다. 같은 비트 열의 배타적 논리합 A ^ A는 항상 0이 되는 성질이 있다. 또 A ^ B를 실행한 결과 C에 C ^ B를 구하면 결과는 A로 된다라고 하는 성질이 있다.

표4 - 배타적 논리합 관계

A B A ^ B
0 0 0
0 1 1
1 0 1
1 1 0

코드3

#include <stdio.h>

int main() {
  int iVar1 = 0xF0F0 , iVar2;
  printf("iVar1 ^ iVar1 = %X\n" , iVar1 ^ iVar1);

  iVar2 = iVar1 ^ 0xABCD;
  printf("iVar2 = %X\n" , iVar2);
  printf("iVar2 ^ 0xABCD = %X\n" , iVar2 ^ 0xABCD);
  return 0;
}

코드3은 변수 iVar1에 0xF0F0을 대입하고 있다. 최초에 printf()는 iVar1끼리의 배타적 논리합을 구하고 있다. 동일한 비트 열의 배타적 논리합은 항상 0이 되는 성질이 있기 때문에, 이 결과가 0이 되는 것을 확인할 수 있을 것이다. 다음 iVar2에 iVar1과 0xABCD의 배타적 논리합을 저장한다. 이 값은 원래 값과는 관계가없는 수치로되어 있습니다 만, iVar2에 다시 동일한 값 0xABCD의 배타적 논리합을 구하면 원래으; 값 iVar1와 동일하다.

마지막으로 1의 보수인데, 이것은 단순히 비트 열을 반전시킨다. ~ 연산자는 다른 비트 연산와 다르게 단항 연산식으로, 주어진 값의 비트열을 반전시킨다. 예를 들어, 비트 맵 이미지에서 각 픽셀의 색상 정보를 얻고, ~ 연산자를 사용하여 비트를 반전시킴으로써 색상을 반전시키는 것도 가능하다.

코드4

#include <stdio.h>

int main() {
  unsigned int iVar = 0xCC33CC33;
  printf("~%X = %X\n" , iVar , ~iVar);
  return 0;
}

코드4는 부호없는 정수 iVar에 0xCC33CC33을 대입하고 있다. 이것은 2진수로 1100 1100 0011 0011 1100 1100 0011 0011는 32 비트 값이지만, 이를 반전 시키면 0011 0011 1100 1100 0011 0011 1100 1100 즉, 16진수 0x33CC33CC라는 값이 된다. 이 프로그램을 실행하면 ~ 연산자는 의해 비트가 반전하고 있는 것을 확인할 수 있을 것이다.

비트 시프트

비트 시프트는 비트 열을 그대로 왼쪽 또는 오른쪽으로 이동시키는 연산이다. 비트 시프트는 시프트 연산자를 사용하여 실행한다. 시프트 연산자와 시프트 연산을 이용한 복합 대입 연산자는 표5와 같다.

표5 - 시프트 연산자

연산자 의미
« 왼쪽 시프트
» 오른쪽 시프트
«= 왼쪽 시프트 대입
»= 오른쪽 시프트 대입

시프트 연산자는 다음과 같이 사용한다.

값 << 시프트수
값 >> 시프트수

<<는 비트를 왼쪽으로 이동하고, >> 비트를 오른쪽으로 이동한다. 값을 몇번을 이동할지를 이동 수를 지정한다. 이동한 만큼 발생한 공백은 0으로 채워진다.

1111 1111 » 2 – 오른쪽으로 2이동 –> 0011 1111

이러한 기능은 도대체 어디에 사용하는 거지라고 생각할지 모른다. 실은 오른쪽으로 이동할 때마다 2로 나눈 결과와 같다. 왼쪽으로 이동 할 때마다 2로 곱한 것과 동일하다. 일반적으로 CPU는 산술 연산보다 시프트 연산을 사용하는 것이 처리가 빠르다는 장점이 있다 (컴파일러에 의해 컴파일시에 최적화되어 버리기 때문에, 변환가 없는 경우도 있다). 그 외에도 32비트 열을 24비트 오른쪽 시프트하여 상위 8비트를 얻는 이용 방법도 있다.

코드5

#include <stdio.h>

int main() {
  int iVar = 100;
  printf("iVar / 4 = %d\niVar * 4 = %d\n" , iVar >> 2 , iVar << 2);
  return 0;
}

이 프로그램은 값이 100인 변수에 대해 오른쪽으로 2번 이동한 값과 왼쪽으로 2번 이동한 값을 표시한다. 2회 이동하는 것은 4를 곱하거나, 나눈 것이 동일하다는 것을 확인할 수 있다.

데이터 형의 크기를 초과 이동한 경우는 잘린다. 또한 부호있는 정수를 오른쪽으로 이동하면, 최상위 비트가 변화하기 때문 부호가 바뀌어 버릴 것이다. 실은 오른쪽 시프트해서 부호는 저장될 수도 있다. 부호있는 정수를 오른쪽으로 이동하면 최상위 비트는 저장된 상태로 이동되고, 부호없는 정수를 오른쪽으로 이동하면 최상위 비트가 0으로 클리어된다. 이를 산술 시프트라고 한다. 반대로, 어떤 상태이든 최상위 비트가 항상 0으로 클리어되는 시프트를 논리 시프트라고 한다. 산술 시프트가 행해지는지, 논리 변화가 일어나는지는 구현에 따라 달라진다. 따라서 이식성있는 프로그램을 작성하는 것이 목적인 경우는 산술 시프트와 논리 시프트 등의 결과에 의존하지 않도록 주의해야 한다.

코드6

#include <stdio.h>

int main() {
  int iVar = -100;
  printf("iVar >> 2 = %d\n" , iVar >> 2);
  return 0;
}

코드6은 -100의 값을 가진 부호있는 정수형 변수 iVar에 대해 오른쪽 시프트를 하고 있는데, 산술 시프트를 하고 있는지, 논리 시프트를 하고 있는지는 프로그램을 실행하는 계산에 따라 다르다.




최종 수정 : 2017-11-26