5-2-다.비트 연산자

비트 연산자는 논리 연산자와 비슷하지만 비트를 연산 대상으로 한다는 점이 조금 다르다. 비트(bit)란 기억 장치의 최소 단위로서 1 또는 0을 기억하며 8개의 비트가 모여야 1바이트가 된다. 32비트의 정수 1234는 16진수로는 0x4d2이며 메모리에 다음과 같이 기억된다.

32비트이므로 32개의 비트가 있고 이 각각의 비트가 1이나 0을 기억함으로써 1234라는 숫자 하나를 저장하는 것이다. 비트 연산자는 이 그림에서 각 격자인 비트들을 대상으로 조작을 한다. 비트가 연산대상이라는 말은 두 피연산자의 대응되는 비트끼리 연산해서 그 결과를 리턴한다는 뜻이다. 일상 생활에서 쓰는 십진수와는 다른 이진수 차원의 연산이라 다소 어려운 것처럼 보이겠지만 막상 다 이해하고 나면 이진수만큼이나 간단하다.

과거 프로그램이 비디오 메모리를 직접 액세스할 때는 비트 연산이 굉장히 중요했으며 섬세한 처리를 하고자 할 때마다 꼭 사용되는 중요한 연산이었다. 비트를 잘 조작하면 반전, 스크롤, 투명 처리 등이 가능했으며 일반적인 산술 연산보다 훨씬 더 빠른 속도로 복잡한 연산을 할 수 있었다. 그러나 윈도우즈 환경에서는 비디오 메모리를 직접 액세스하는 것이 금지되었고 별로 그럴 필요도 없기 때문에 요즘은 비트 연산자가 많이 사용되지 않는다.

하지만 게임이나 중요한 시스템 소프트웨어에서는 아직도 비트 연산이 꼭 필요하며 활용 범위가 넓다. 스타일값 중 원하는 값을 추출하거나 액세스 권한 같은 플래그를 다룰 때 비트 연산자가 사용된다. 일단 비트 연산자의 종류에 대해 표로 간단하게 정리해 보자. 다음 여섯 가지가 있다.

 

연산자

설명

~

비트를 반전시킨다.

&

대응되는 비트가 모두 1 1이다.

|

대응되는 비트가 모두 0 0이다.

^

개의 비트가 달라야 1이다.

<<

지정한 수만큼 왼쪽으로 비트들을 이동시킨다.

>>

지정한 수만큼 오른쪽으로 비트들을 이동시킨다.

 

~만 단항 연산자이고 나머지는 모두 두 개의 피연산자를 취하는 이항 연산자이다. 비트 연산은 정수 수준에서만 의미가 있기 때문에 피연산자는 모두 정수형이거나 또는 정수로 자동 변환될 수 있는 타입이어야 한다. 실수나 포인터 등은 비트 연산자와 함께 사용할 수 없다. 다음은 비트 연산자들의 진리표인데 다 알고 있겠지만 도표로 정리해 보도록 하자.

 

b1

b2

b1 & b2

b1 | b2

b1 ^ b2

~b1

0

0

0

0

0

1

0

1

0

1

1

1

1

0

0

1

1

0

1

1

1

1

0

0

 

단항 연산자 ~는 가장 이해하기 쉬운 연산자이다. 비트가 1이면 0으로 0이면 1로 바꾸어 1의 보수로 만든다. a가 0x59라고 할 때 ~a가 어떻게 연산되는지 보자. 32비트 환경에서 정수는 32비트이지만 설명의 편의상(사실은 그림 그리기 귀찮으니까) a가 8비트 정수타입(unsigned char)이라고 하자.

0x59의 ~연산 결과는 비트를 모두 뒤집은 0xa6이 되는데 이 두 수는 1의 보수 관계이며 더하면 전체 비트가 모두 1인 0xff(이진수로 11111111)가 된다. 이렇게 그림으로 비트들이 어떻게 변하는지 보면 ~연산을 쉽게 이해할 수 있을 것이다. 만약 10진수로 이 연산자의 동작을 살펴보면 89의 ~연산 결과가 166이 되는데 89가 어떻게 166이 되었는지 직감적으로 이해하기 어렵다. 그래서 비트 연산자를 설명할 때는 2진수나 16진수를 쓸 수밖에 없으며 이 동작을 잘 이해하기 위해서는 2진수와 16진수 사이를 암산으로 신속하게 변환할 수 있어야 한다.

반전 연산자는 이미지 처리에 많이 사용되는데 이미지의 각 픽셀값을 반대로 뒤집으면 역상의 이미지를 얻을 수 있다. 흰색은 검정색이 되고 검정색은 흰색이 되기 때문에 역상 이미지가 만들어지는 것이다. Win32 환경에서는 API 함수들이 이런 처리를 대신해 주기 때문에 이 연산자를 직접 쓸 경우는 드물다.

&, | 연산자의 동작도 이해하기 쉬운데 특정 비트만 0으로 만들거나 또는 1로 만들 때 이 연산자들이 사용된다. a가 0x59일 때 a & 0xf(이진수 00001111)가 어떻게 연산되는지 보자.

&연산의 진리표를 보면 0과 &되는 비트는 그 값에 상관없이 무조건 0이 되며 1과 &되는 비트는 원래 비트값을 그대로 유지하는 특성이 있다. 이진수 00001111과 &연산을 하면 상위 4비트는 0이 되며 하위 4비트만 값을 유지한다. 이런 식으로 특정 비트를 강제로 0으로 만드는 연산을 마스크 오프(mask off)라고 한다. | 연산은 이와는 반대의 연산을 한다.

1과 |되는 비트는 무조건 1이 되고 0과 |되는 비트는 원래 값을 유지하는데 이렇게 특정 비트를 강제로 1로 만드는 연산을 마스크 온(mask on)이라고 한다. 마스크 연산이란 특정 비트에 덮개(mask)를 씌워 놓고 전부 0(off)이나 1(on)로 만든 후 덮개를 벗긴다고 생각하면 된다. 덮개가 씌워져 있던 비트는 원래 값을 유지하고 나머지 비트는 0이나 1로 강제 변환된다. & 연산에서는 1이 마스크이고 OR 연산에서는 0이 마스크이다.

&, | 연산자는 일부 비트만 제한적으로 읽거나 변경할 때 흔히 사용된다. 기억 공간을 절약하기 위해 하나의 정수값을 비트별로 잘라 여러 가지 값을 같이 기억시키는 방법이 많이 사용되는데 예를 들어 한글 조합형 코드는 16비트 길이를 가지며 다음과 같이 구성되어 있다.

최상위 비트는 항상 1인데 이 값은 이 코드가 한글임을 표시한다. 영문 알파벳은 모두 128보다 작기 때문에 이 비트가 0으로 되어 있어 한글과 구분된다. 16비트의 정수값을 5비트씩 잘라서 초성, 중성, 종성 코드를 기억시킨다. 한글 낱글자인 ㄱ,ㄴ,ㄷ,ㄹ,... 은 총 개수가 32개가 안되기 때문에 5비트면 낱글자 하나를 기억할 수 있고 이런 글자 세 개가 모이면 한글 1음절을 표현할 수 있다. 초성, 중성, 종성 코드를 각각의 정수에 기억하는 방법에 비해 훨씬 더 기억 공간이 절약된다. 이런 조합된 값에서 일부만 추출해 내거나 일부만 변경하려면 &, | 비트 연산이 필요하다. 다음은 한글 1음절의 값을 가지는 변수 Han을 비트 조작하는 예이다.

 

Han & 0x1f                  // 종성만 분리한다.

Han & 0x7c00              // 초성만 분리한다.

Han & 0xffe0 | 2      // 종성만 ㄱ으로 바꾼다.

 

윈도우의 스타일도 32비트의 정수에 각 스타일 비트들이 조합되어 있는데 이런 값들을 조작할 때도 비트 연산자가 사용된다. 다음에 API를 배울 때 보게 되겠지만 간단히 예만 보이자면 다음과 같다. style 변수에 32개나 되는 스타일 비트가 기억되어 있는데 다른 스타일값은 무시하고 WS_CHILD 값만 조사하거나 변경하고자 할 때 마스크 연산을 해야 한다.

 

if (style & WS_CHILD)            // WS_CHILD 스타일을 가지고 있으면

style |= WS_CHILD           // WS_CHILD 스타일 지정

 

XOR 연산자인 ^ 는 배타적 논리합이라고 부르며 ~연산자와 마찬가지로 비트를 반전시키는 기능을 하는데 ~연산자가 전체 비트를 반전시키는 반면 ^는 지정한 비트만을 반전시킨다. 배타적 논리합은 비트가 서로 다를 때만 1이 되고 같으면 0이 되기 때문에 1과 ^되는 비트는 반전되고 0과 ^되는 비트는 원래 값을 유지한다. 그래서 반전시키고자 하는 부분만 1로 만든 값과 ^연산을 취하면 원하는 부분만 반전된다. 이름하여 마스크 반전이라고 할 수 있다.

반전된 값은 다시 반전시키면 원래대로 돌아오는 특성이 있다. 1을 0으로 만들었다가 다시 반전하면 원래값 1이 되기 때문이다. 그래서 XOR 연산은 이미지의 이동이나 반복적인 점멸 처리에 사용된다. 캐럿이 깜박거리거나 텍스트의 선택 블록을 보여주는 처리가 모두 이 연산을 사용하는데 반복적으로 XOR 연산을 하면 원형을 손상하지 않고 복구 가능하기 때문이다.