참조저자: PJ Arends
역저: 오상문 (sualchi@hanmail.net)
비트 연산 이해하기 [수알치의]
소개
어떤 사람들은 비트(bitwise) 연산을 어렵게 여긴다. 여기에 비트 연산과 그 사용법을
간단하게 정리해서 제공하니 많은 도움이 되기를 바란다.
비트를 살펴보자
비트는 무엇인가?
쉽게 말해서 비트는 우리가 컴퓨터에서 다루는 모든 내용의 기초가 되는 것으로써,
1 또는 0 값을 가질 수 있는 단위이다. 여러분이 사용할 모든 데이터는 비트를 이용하여
여러분의 컴퓨터에 저장된다. 그리고 8개 비트를 묶어서 1 바이트라고 부르며, 2 바이트
즉 16개 비트를 묶어서 워드라고 부른다. 마찬가지로 2 워드 즉 32개 비트를 묶어서
더블워드라고 부른다.
* 비트(bit) = 0 또는 1 값을 가질 수 있는 저장 단위
* 바이트(BYTE) = 8 개 비트 묶음
* 워드(WORD) = 16개 비트 묶음
* 더블워드(DWORD) = 32개 비트 묶음
비트 연산은 바이트, 워드, 더블워드 단위를 이용하여 작은 배열이나 구조체를 다루는
것처럼 사용한다. 여러분은 비트 연산자를 사용하여 개별 비트 값을 알아내거나 저장할
수 있으며, 여러 비트를 동시에 다룰 수도 있다.
16진수와 비트의 절묘한 만남
비트를 다룰 때 각 비트를 0과 1로 표현하는, 즉 2진 표기법으로 다루기가 곤란하거나
불편한 경우가 있다. 그 대신에 16진수 기반으로 접근하는 방법을 사용할 수 있다.
이미 아는 내용일지도 모르겠지만, 16진수는 0~15까지의 숫자(0~9와 A,B,C,D,E,F로
표기)를 이용해 4개 비트 묶음을 취한다. 이 4개의 비트 묶음(즉 바이트의 절반인 셈이다)은
니블이라고 한다.
*수알치 왈 : 니블(nibble)은 모 컴퓨터 잡지의 이름으로도 사용되었다.
니블 16진수 값 ====== ========= 0000 0 0001 1 0010 2 0011 3 0100 4 0101 5 0110 6 0111 7 1000 8 1001 9 1010 A 1011 B 1100 C 1101 D 1110 E 1111 F
그러므로 문자 'r'(아스키 코드 114번)을 가진 1 바이트는 다음처럼 나타낼 수 있다.
0111 0010 2진수
7 2 16진수
그리고 이것은 '0x72'처럼 표기한다.
비트 연산자
여섯 가지의 비트 연산자가 존재한다.
|
& |
AND 연산자 |
|
| |
OR 연산자 |
|
^ |
XOR 연산자 |
|
~ |
1의 보수 또는 NOT 연산자 |
|
>> |
오른쪽 쉬프트 연산자 |
|
<< |
왼쪽 쉬프트 연산자 |
*수알치 왈 : 쉬프트(shift)는 한 칸 또는 여러 칸을 밀어내는 것이다.
& 연산자
AND를 의미하는 & 연산자는 두 값을 비교한 다음에 비교하는 두 비트 값이 모두 1이면
1을 돌려주고, 아닌 경우에는 0으로 돌려준다. 즉, 다음과 같은 표를 이용하여 처리한다.
A B 결과
------------------
1 & 1 == 1
1 & 0 == 0
0 & 1 == 0
0 & 0 == 0
& 연산자를 활용하는 좋은 예는 어떤 비트의(또는 여러 비트들의) 값을 체크하기 위한
마스크 값으로 사용하는 것이다.
예컨대 어떤 비트 플래그들을 가진 한 바이트가 있다고 하자.
*수알치 왈 : 마스크(mask)는 원하는 부분만 얻어내기 위해 사용하는 필터 값이다.
그리고 비트 4(비트는 0번 위치에서 시작하므로 다섯 번째 비트를 의미함)가 세트된
상태인지 아닌지, 즉 그 비트 값이 값이 무엇(1/0)인지 검사하고 싶다면
다음처럼 처리할 수 있다.
*수알치 왈 : 비트가 1이면 세트된(set) 상태이고, 0이면 클리어(clear) 상태라고 부른다.
BYTE b = 50; // 바이트 형 변수 b는 50 값을 가진다고 가정함.
if ( b & 0x10 ) // 0x10은 2진수로 표현하면 '00010000'이다.
cout << "Bit four is set" << endl;
else
cout << "Bit four is clear" << endl;
이것은 다음과 같은 계산 방식과 같은 결과를 가진다.
00110010 - 변수 b & 00010000 - & 0x10 ---------- 00010000 - 계산 결과
비트 4에 1을 AND한 결과가 1이 나오려면 당연히 비트 4의 값이 1이어야 하므로,
우리는 이 계산 결과를 통해서 비트 4의 값이 1이라는 것을 알아낼 수 있다.
| 연산자
수알치 왈: OR 연산에 사용하는 | 문자가 폰트 종류에 따라서는 1자처럼 보인다.
하지만 중간 허리가 끊긴 파이프 표시이며, 글자판의 \ 키에서 찾을 수 있다.
| (OR) 연산자는 두 값을 비교한 후, 하나 또는 양쪽이 세트된 상태(1)이면 1을 돌려준다.
즉, 다음과 같은 표를 이용하여 처리한다.
A B 결과
------------------
1 | 1 == 1
1 | 0 == 1
0 | 1 == 1
0 | 0 == 0
OR 연산은 어떤 원하는 비트 값만 세트할 때, 즉 1 값으로 지정할 때 활용할 수 있다.
예컨대 비트 2(즉 세 번째 비트)를 무조건 1로 세트하고 싶다면 다음처럼 한다.
BYTE b = 50;
BYTE c = b | 0x04;
cout << "c = " << c << endl;
이것은 다음과 같은 계산 결과를 가진다.
00110010 - b
| 00000100 - | 0x04
----------
00110110 - 결과
^ 연산자
XOR 연산을 하는 ^ 연산자는 두 값을 비교한 후에 두 값이 같으면 0, 아니면 1을 돌려준다.
즉, 다음과 같은 표를 이용하여 처리한다.
A B 결과
------------------
1 ^ 1 == 0
1 ^ 0 == 1
0 ^ 1 == 1
0 ^ 0 == 0
XOR 연산의 가장 좋은 예는 어떤 비트들을 반전시키는(toggle) 것이다. 예컨대 비트 3과
4번을 반전하는 즉 1을 0으로, 0을 1로 만드는 방식은 다음과 같다.
BYTE b = 50; // 임의의 바이트 값
cout << "b = " << b << endl;
b = b ^ 0x18; // 0x18 = 2진수 00011000
cout << "b = " << b << endl;
b = b ^ 0x18;
cout << "b = " << b << endl;
이것은 다음과 같은 계산 결과를 가진다.
00110010 - b
^ 00011000 - ^ 0x18
----------
00101010 - 결과 (첫 번째)
00101010 - b
^ 00011000 - ^ 0x18
----------
00110010 - 결과 (두 번째)
~ 연산자
~ (1의 보수 또는 반전) 연산자는 오직 하나의 값에 동작하며, 각 모든 비트 값에 대해서
1은 0으로, 0은 1로 반전시킨다.
이것의 좋은 사용 예를 들면, 데이터 크기에 관계 없이 다른 비트들을 1로 설정하면서
원하는 비트들을 0으로 설정하는 경우이다. 예컨대 비트 0과 1을 제외한 다른 비트를
1로 세트하려면 다음처럼 한다.
BYTE b = ~0x03; // 0x03은 2진수로 00000011
cout << "b = " << b << endl;
WORD w = ~0x03; // 워드(2 바이트)에 이용될 때는 0000000000000011로 자동 확장
cout << "w = " << w << endl;
이것은 다음과 같은 계산 결과를 가진다.
00000011 - 0x03
11111100 - ~0x03 (바이트 단위에서)
0000000000000011 - 0x03
1111111111111100 - ~0x03 (워드 단위에서)
다른 예를 들면, 어떤 비트들을 0으로 설정하기 위해서 & 연산자와 함께 사용하는 것이다.
비트 4번을 클리어(0으로 지정)하는 예는 다음과 같다.
BYTE b = 50; // 임의의 바이트 값
cout << "b = " << b << endl;BYTE c = b & ~0x10; // ~0x10은 2진수로는 11101111이다.
cout << "c = " << c << endl;
이것은 다음과 같은 계산 결과를 가진다.
00110010 - b & 11101111 - ~0x10 ---------- 00100010 - 결과 (4번 비트가 0으로 바뀌었다)
>> 및 << 연산자
>> (오른쪽 쉬프트)와 << (왼쪽 쉬프트) 연산자는 지정된 비트 위치만큼 각 비트 값들을
이동시킨다(밀어낸다).
>> 연산자는 상위 비트에서 하위 비트 방향으로 이동시킨다.
<< 연산자는 하위 비트에서 상위 비트 방향으로 이동시킨다.
이들 연산자는 MAKEWPARAM, HIWORD, LOWORD 매크로 등에서 비트를 재배치하기
위해 사용된다.
BYTE b = 12; // 임의의 바이트 값
cout << "b = " << b << endl;BYTE c = b << 2; // 2 비트만큼 왼쪽으로 밀기
cout << "c = " << c << endl;c = b >> 2; // 2 비트만큼 오른쪽으로 밀기
cout << "c = " << c << endl;
이것은 다음과 같은 계산 결과를 가진다.
00001100 - b
00110000 - b << 2
00000011 - b >> 2
비트 필드 다루기
또 다른 흥미로운 것은 구조체의 비트 필드를 이용하여 비트들을 다루는 것이다.
비트 필드는 BYTE, WORD, DWORD를 구성하는 비트들을 마치 구조체의 멤버 변수를
다루듯이 처리할 수 있게 해준다. 에컨대 년월일 날짜 데이터를 최소한의 메모리를
사용하는 형태로 구성하고 싶다면, 다음처럼 작성할 수 있다.
struct date_struct {
BYTE day : 5, // 1 ~ 31 (이것은 날짜를 의미)
month : 4, // 1 ~ 12 (이것은 달을 의미)
year : 14; // 0 ~ 9999 (이것은 년도를 의미)
} date;
이 경우에 날짜는 최하위 5개 비트, 달은 다음의 4개 비트, 년도는 그 다음의 14개
비트를 차지한다. 그러므로 우리는 23 비트 크기의 날짜 구조체에 저장할 수 있으므로,
총 3 바이트 안에 들어가게 된다. 3 바이트 즉 24 비트 크기에서 가장 마지막인 24 번째
비트는 사용하지 않는다. 만약 각 비트 필드를 정수형(그리고 정수가 4 바이트 크기)이면,
그 구조체는 12 바이트로 구성될 것이다.

이제 이렇게 저장된 것에서 부분을 구해보자.
- 먼저 비트 필드 구조체의 데이터 형을 살펴봐야 한다. 이 경우에는 BYTE 형을 사용했다.
BYTE는 8 비트 크기이므로, 컴파일러는 BYTE를 저장하기 위한 할당할 것이다.
우리는 8 비트보다 더 크게 사용하고 있는데, 컴파일러는 그것을 할당할 수 있을만큼
컴파일러는 추가 BYTE를 자동으로 더 할당한다. 만약 우리가 WORD 또는 DWORD를
사용했다면, 컴파일러는 우리 구조체를 가질 수 있는 총 32 비트를 할당했을 것이다.
- 얼마나 다양한 필드가 선언되어 있는지 살펴보자. 우리는 day, month, year
변수를 가지고 있고, 그 변수 뒤에 콜론을 넣고 다음에 비트 크기를 적었다.
각 비트 필드는 콤마에 의해 구분되며, 가장 최종 비트 필드 뒤에는 세미콜론을 붙인다.
- 다음에는 구조체 선언에서 값을 얻어보자. 비트 필드 접근도 일반적인 구조체 멤버에
접근하는 표기법을 사용한다. 마찬가지로 그 구조체의 주소를 이용해 접근할 수도 있다.
date.day = 12; // day 비트 필드에 값을 넣음
dateptr = &date; // 구조체 data의 주소를 구함
dateptr->year = 1852; // 주소(포인터)를 이용한 비트 필드에 값 넣기
수알치 왈: 비트 필드는 특히 2바이트 한글 코드 처리에서 유용하다.
즉 한글의 초.중.종성 코드를 합치거나
분리할 때 요긴하게 사용된다.
| <이상> |
|
