비트필드(비트 구조체)

비트 구조체

정의

비트 구조체는 비트들을 멤버로 가지는 구조체이며 비트 필드(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비트를 버림으로써 속도를 증가시키는 것이 현명한 선택이다.

 이름이 없는 비트의 크기를 0으로 지정할 수 있는데 이렇게 되면 현재 워드의 미사용 비트를 모두 버린다. 크기가 0인 멤버 다음의 멤버는 새로운 워드의 경계에 배치됨으로써 역시 속도를 증가시키는 효과가 있다. 다음 선언문은 두 번째 멤버에 이름도 주지 않고 비트 크기도 0으로 지정하고 있다.

 

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비트를 할당할 수 없다.

 비트 멤버는 값을 읽을 수도 있고 쓸 수도 있는 좌변값이다. 일반적으로 좌변값은 & 연산자를 사용할 수 있지만 비트 멤버에 대해서는 예외적으로 & 연산자를 사용할 수 없다. 비트 멤버는 메모리를 점유하고는 있지만 바이트 단위의 주소를 가지는 것은 아니며 구조체 속의 일부로 존재할 뿐이다. 좌변값이면서 &연산자를 쓸 수 없는 대상은 비트 필드와 register 기억 부류 두 가지가 있다.

 당연한 얘기가 될 지 모르겠지만 비트 필드와 일반 멤버를 한 구조체에 같이 선언할 수도 있다.

 

struct tag_bit {

     int Value;

     unsigned Grade:6;

     unsigned Score:8;

     unsigned Male:1;

     double Rate;

};

 

이렇게 되면 정수 멤버 Value가 처음 4바이트를 차지하고 다음의 4바이트를 비트 필드들이 차지하고 뒷 부분에 실수 멤버 Rate가 배치될 것이다.


활용

비트 구조체는 메모리를 구성하는 최소 단위인 1비트까지도 알뜰하게 사용할 수 있다는 것이 장점이다. 여러 가지 값들을 꼭 필요한만큼 비트를 잘게 쪼개 값을 기억시킬 수 있으므로 메모리 효율이 아주 좋다. 비트 구조체를 어떤 경우에 사용하는지와 그 장점은 무엇인지 가상적인 게임을 만들면서 자료 구조를 설계해 보도록 하자.

여기서 만들 게임은 일종의 슈팅 게임으로서 화면 위에서 날아오는 적들을 아래쪽의 대포로 쏘아 맞추는 게임이다. 적이 한 종류밖에 없으면 게임의 재미가 없으므로 다양한 종류의 적들을 만들기로 하자. 다음 세 가지 종류를 만든다.

 

적 유형 0 : 왕파리

적 유형 1 : 쇠파리

적 유형 2 : 초파리

 

각 유형별로 색상도 빨강, 파랑, 노랑, 초록 4가지가 있으며 적이 움직이는 방향도 좌에서 우로, 우에서 좌로 두 가지가 있다고 하자. 또한 총알을 많이 발사하는 못된 놈이 있는가 하면 좀 덜 발사하는 쉬운 놈도 있고 한 번 맞아서 죽는 놈도 있고 4번까지 때려야 죽는 끈질긴 놈도 있다. 등장하는 적이 이 정도로 다양해야 게임이 재미있어질 것이다.

그렇다면 이런 적들에 대한 정보를 저장할 수 있는 자료 구조를 만들어 보자. 여러 가지 정보를 하나의 단위로 묶어야 하므로 구조체가 적당하며 최대 100개까지의 적이 한꺼번에 화면에 나타날 수 있도록 하자면 크기 100의 구조체 배열이 필요하다. 다음이 초안이다.

 

struct tag_enemy {

     int type;

     int color;

     int movetype;

     int bullet;

     int strike;

} enemy[100];

 

이렇게 하면 정보를 저장하는데 아무 문제가 없다. 그러나 이 구조체를 다시 살펴보면 기억 장소를 굉장히 많이 낭비하고 있다는 것을 알 수 있다. 적 유형은 0, 1, 2 셋 중 하나인데 이 정보를 저장할 type 멤버는 -20억~20억까지의 큰 값을 기억할 수 있는 4바이트의 int형으로 되어 있다. short나 char형으로 하면 어느 정도 낭비를 줄일 수 있지만 그래도 낭비는 발생한다. 다른 멤버들도 마찬가지로 int형의 전 범위를 다 사용하지도 않는데 과다하게 기억 장소를 낭비하고 있다. 이럴 때 비트 구조체를 사용한다.

 

struct tag_enemy {

     unsigned char type:2;

     unsigned char color:2;

     unsigned char movetype:1;

     unsigned char bullet:1;

     unsigned char strike:2;

} enemy[100];

 

이렇게 비트 구조체로 바꾸면 적에 대한 정보를 모두 기억시키는 데 단 1바이트밖에 사용하지 않는다. 적 유형은 세 가지가 있으므로 2비트면 충분하고 색상도 네 가지뿐이므로 2비트이면 충분하다. 8비트의 그 좁은 영역에 한 비트, 두 비트씩 잘라서 모든 정보들을 다 기억시킬 수 있다. 더구나 enemy는 크기 100의 배열이기 때문에 절약의 효과가 더욱 두드러진다.

비트 구조체는 메모리를 최대한 절약해야 할 때 사용하는데 요즘은 비트 구조체를 사용하는 경우가 흔하지 않다. 왜냐하면 과거 메모리가 비쌀 때는 단 1바이트라도 아끼기 위해 이런 복잡한 구조체를 사용했지만 요즘은 메모리가 워낙 풍부해져서 이런 구두쇠짓을 굳이 할 필요가 없어졌다. 더구나 비트 구조체는 크기가 작은만큼 속도에는 불리한 단점이 있으므로 웹로그같은 대용량의 정보를 저장할 때 외에는 가급적이면 사용을 자재하는 것이 좋다.


출저:www.winapi.co.kr



즉!

비트필드는 메모리 공간을 효율적으로 활용하기 위해서

비트단위로 쪼개어 구조체를 만들어 사용하는 것인데,

요즘은 잘 사용하지 않는다.

생각해보면 당연하다....내 하드가 1TB가 넘는걸?....

ㅋㅋㅋㅋ


이 글을 공유하기

댓글

Designed by JB FACTORY