13-4.비트 구조체
13-4-가.정의
비트 구조체는 비트들을 멤버로 가지는 구조체이며 비트 필드(bit field)라고도 부른다. 잘 알다시피 비트는 기억의 최소 단위이며 0 또는 1중 하나를 기억한다. 비트 하나로 2가지 경우, 두 개로 4가지 경우까지 기억할 수 있으며 비트 세 개가 모이면 8가지의 다른 수를 기억시킬 수 있는데 일반적으로 표현하자면 비트 n개가 모이면 2n개의 숫자를 표현할 수 있다.
멤버가 가질 수 있는 값의 범위가 아주 작다면 32비트의 int나 8비트의 char보다 더 작은 단위로 비트를 쪼개 알뜰하게 정보를 기억시킬 수 있는데 이때 사용하는 것이 비트 구조체이다. 비트 구조체를 선언하는 기본 형식은 다음과 같다.
struct 태그명 {
타입 멤버1:비트수;
타입 멤버2:비트수;
타입 멤버3:비트수;
....
};
각 멤버 이름 다음에 이 멤버의 비트 크기를 적는다. 멤버의 타입은 원칙적으로 정수만 가능하며 부호의 여부에 따라 unsigned int 또는 signed int 둘 중 하나의 타입을 지정한다. 그 작은 공간에도 최상위의 1비트를 부호 비트로 할당할 수 있는데 사실 비트로 표현해야 할 정보는 수치값이라기보다 일종의 기호나 표식인 경우가 많기 때문에 부호를 쓰는 경우는 드물다. 따라서 비트 구조체의 멤버들은 통상 unsigned 타입이다.
원래 C언어 스팩에는 비트 멤버의 타입은 int 또는 unsigned 중 하나만 가능하도록 되어 있으나 마이크로소프트의 비주얼 C++은 short, long, char 등 정수와 호환되는 모든 타입을 허용한다. 그래서 비트 멤버의 타입으로는 모든 정수형을 다 사용할 수 있으며 멤버의 타입에 따라 비트 필드 전체의 크기가 달라진다. double이나 포인터, 배열 따위는 비트 필드가 될 수 없다. 다음 예제는 비트 구조체를 사용하는 가장 간단한 예제이다.
예 제 : BitField |
#include <Turboc.h>
struct tag_bit {
unsigned short a:4;
unsigned short b:3;
unsigned short c:1;
unsigned short d:8;
};
void main()
{
tag_bit bit;
bit.a=0xf;
bit.b=0;
bit.c=1;
bit.d=0xff;
printf("크기=%d, 값=%x\n",sizeof(bit),bit);
}
tag_bit 타입에 a, b, c, d 네 개의 멤버가 포함되어 있는데 각각 4, 3, 1, 8 비트씩을 차지한다. 멤버가 선언된 순서대로 하위 비트에서 순서대로 할당되며 구조체 자체의 크기는 모든 비트 멤버의 총 비트수와 같다. tag_bit 타입은 총 16비트이므로 2바이트를 차지하며 메모리상에 다음과 같이 생성된다. 비트 필드가 선언 순서대로 MSB에 저장될 지, LSB에 저장될지는 컴파일러마다 다른데 일반적으로 오른쪽(LSB)부터 채워 나간다.
a가 먼저 선언되었으므로 a가 가장 하위 비트에서 시작되는데 4비트 크기로 선언되었으므로 a는 0~15까지 16가지 경우의 수를 기억할 수 있다. 다음으로 b가 3비트, c가 1비트를 차지하며 d가 8비트를 차지하여 총 크기는 16비트가 된다. 각 멤버들은 자신이 차지하고 있는 비트수만큼의 수를 기억할 수 있는데 비트 멤버를 참조할 때는 일반 구조체처럼 멤버 연산자를 사용하면 된다.
bit.a=0xf 대입문에 의해 bit의 b0~b3까지 하위 4비트에 0xf(이진수 1111)이 기억되며 bit.b=0 대입문에 의해 b4~b6까지 3비트에 0이 기억될 것이다. 16비트중 중간 비트에만 값을 대입하려면 쉬프트, 마스크 오프, 비트 OR 등의 복잡한 연산을 해야 하지만 비트 멤버에 값을 대입할 때는 이런 복잡한 동작을 컴파일러가 대신한다. bit.c=1 대입문에 의해 b7만 1이 되며 bit.d=0xff 대입문은 상위 8비트만 모두 1로 바꿀 것이다.
위 예제의 실행 결과는 "크기=2, 값=ff8f"가 되는데 16비트이므로 크기는 2바이트이며 a, b, c, d 각각에 대입한 값들은 이진수로 1111111110001111이 된다. 다음은 비트 구조체의 특징 및 주의 사항이다.
struct tag_bit {
unsigned short a:4;
unsigned short b:3;
unsigned short :1;
unsigned short d:8;
};
이렇게 되면 세 번째 멤버는 괜히 1비트를 그냥 버리는 역할만 한다. 이런 것이 왜 필요한가 하면 바이트나 워드의 경계에 걸치면 값을 읽고 쓸 때 쉬프트 연산을 해야 하며 따라서 속도가 떨어지기 때문이다. 그래서 다음 멤버가 바이트의 처음부터 시작할 수 있도록 1비트를 버리기 위해 이름없는 멤버를 하나 적어준다. 어차피 메모리는 바이트 단위이므로 1비트를 버린다고 해서 기억 장소가 낭비되는 것은 아니다. 15비트나 16비트나 어차피 필요한 메모리는 2바이트이므로 1비트를 버림으로써 속도를 증가시키는 것이 현명한 선택이다.
struct tag_bit {
unsigned short a:4;
unsigned short :0;
unsigned short c:1;
unsigned short d:8;
};
이렇게 되면 a가 최하위 4비트를 차지하며 c는 다음 워드 경계에서 새로 시작된다. 즉 a왼쪽의 12비트가 버려진다.
int a:33;
short b:17;
비트 멤버란 길이가 긴 비트 중 일부만 값 저장에 사용하는 것인데 타입보다 더 큰 비트 크기를 가지는 것은 불가능하다. int형은 32비트 크기를 가지므로 a 멤버에 33비트를 할당할 수 없다.
struct tag_bit {
int Value;
unsigned Grade:6;
unsigned Score:8;
unsigned Male:1;
double Rate;
};
이렇게 되면 정수 멤버 Value가 처음 4바이트를 차지하고 다음의 4바이트를 비트 필드들이 차지하고 뒷 부분에 실수 멤버 Rate가 배치될 것이다.