[2-12] 비트 필드

비트 필드는 사용자 지정 자료형인 구조체의 구성 요소를 비트 단위로 나누는 기법입니다. 다음과 같이 선언합니다.

struct 구조체명 {
자료형 변수명:  비트수;
자료형 변수명:  비트수;
자료형 변수명:  비트수;
...
}

예를 들어 8비트를 다음과 같이 비트 필드로 나눠서 성별/학년/반을 나타낼 수 있습니다.

struct BF{
unsigned int sex: 1;   // 1비트면 남성/여성을 표현할 수 있습니다.
unsigned int grade: 3; // 초등학교는 3비트면 표현할 수 있습니다.
unsigned int room: 4; // 10학급까지 있다면 4비트로 표현할 수 있습니다.
}

변수 선언 방법은 다음과 같습니다.

struct BF bitfield;

이 구조체를 그림으로 보면 다음과 같습니다.

[그림] 구조체 bitfield의 메모리 구조

구조체 bitfield의 메모리 구조

비트 필드 초기화

초기화는 다음과 같이 두 가지 방법이 있습니다.

  1. 선언하면서 초기화하기
  2. 선언 후에 초기화 하기

첫 번째 방법은 구조체와 같습니다.

struct BF bitfield = {"관우", 1, 2, 3};

두 번째 방법 역시 구조체와 같습니다.

struct BF bitfield;
bitfield.sex = 1;      <= 1 대입
bitfield.grade = 3;  <= 3 대입
bitfield.room = 4;   <= 4 대입

기존에 접한 방법이라 어렵지 않을 겁니다.

비트 필드 사용하기

비트 필드를 사용하지 않은 멤버 변수와 사용법은 완전히 같습니다. 점 연산자만 찍어주면 되죠! 사용법을 예제로 알아봅시다.

#include <stdio.h>

struct BF{
unsigned int sex: 1;   // 1이면 남성, 0이면 여성
unsigned int grade: 3; // 초등학교는 3비트면 표현할 수 있습니다.
unsigned int room: 4; // 10학급까지 있다면 4비트로 표현할 수 있습니다.
};

int main(void) {
	
	struct BF bitfield;
	int s;
	int g;
	int r;
	
	printf("정보를 입력하세요(sex, grade, room)\n");
	scanf("%d %d %d\n", &s, &g, &r);
	
	bitfield.sex = s;
	bitfield.grade = g;
	bitfield.room = r;
	
	if(bitfield.sex){
		printf("남성 : %d학년 %d반", bitfield.grade, bitfield.room);
	}
	else{
		printf("여성 : %d학년 %d반", bitfield.grade, bitfield.room);
	}
	
	return 0;
}

[출력]

정보를 입력하세요(sex, grade, room)
1 2 3
남성 : 2학년 3반

[출력]

비트 필드를 사용하면 메모리 공간을 비트 단위로 다룰 수 있고 메모리 공간을 절약할 수 있습니다.

그런데 의아한 점이 하나 있군요? 예제에서 scanf()로 값을 받는 코드를 다시 살펴보겠습니다.

scanf("%d %d %d\n", &s, &g, &r);

구조체 비트 필드 멤버 변수에 직접 scanf를 사용하지 않고 s, g, c 변수를 따로 선언해 scanf()로 받은 값을 비트 필드에 대입하는군요.

왜 그랬을까요? 그 이유는 제 친구 제갈량이 설명해줄 겁니다. 구미가 당긴다면 한 번 읽어보세요.

제갈량의 동남풍 : 비트 단위로 메모리 접근하기

그리고 놀라운 하나! 비트필트는 & 연산자를 사용해 값을 할당할 수 없습니다. 따라서 다음과 같은 scanf()는 컴파일 에러를 발생합니다.

	scanf("%d\n", &bitfield.sex);

& 연산자는 주소 연산을 하게 되는데 바이트 단위로 연산하기 때문입니다. 즉 비트단위로 접근을 못 합니다. 예를 들어 char형 배열의 출력을 잠깐 살펴보겠습니다.

#include <stdio.h>

int main(void) {
	char test[4];
	
	for(int i ; i < sizeof(test); i++)
	{
		printf("0x%x\n", &test[i]);
	}
	// your code goes here
	return 0;
}

[결과]

0xb0227ad0
0xb0227ad1
0xb0227ad2
0xb0227ad3

char형 배열의 주소를 출력했더니, 맨 마지막 주소가 1씩, 즉 1바이트씩 변하죠? int형 배열이었다면 당연히 4씩 증가했을 겁니다. 여기서 1은 1바이트죠. 주소를 표현하는 최소 단위가 1바이트인 겁니다. 따라서 비트 단위로 주소를 접근할 방법이 애초에 없는 겁니다. 그래서 &연산을 지원하지 않습니다.

이상 제갈량이었습니다.

비트 필드 사용 팁팁팁

몇 가지 비트 필드를 사용하는 팁을 알려드릴게요.

  1. 정의와 선언을 동시에 하다.
  2. 바이트 단위로 나누기
  3. 비트 건너뛰고 할당하기
  4. 자료형 생략하기
  5. signed는 지정은 무의미해

정의와 선언 동시에 하기

구초제와 마찬가지로 정의와 선언을 동시에 할 수도 있다.

struct BF{
unsigned int sex: 1;
unsigned int grade: 3;
unsigned int room: 4;
} bitfield; <= 구조체 BF형 변수 bitfield 선언

바이트 단위로 나누기

요소의 비트 크기를 0으로 하면 해당 위치에서 바이트를 나누라는 의미입니다.

struct BF{
unsigned int sex: 1;
unsigned int contry: 5;
unsigned int boundary: 0; <= 여기가 바이트 경계
unsigned int grade: 3;
unsigned int room: 4;
};

왜 바이트 나누기를 할까요? 메모리 주소는 바이트 단위로 접근할 수 있다고 했죠? 위에서 구역을 나누지 않으면 grade가 첫 번째 바이트와 두 번째 사이에 걸치게 되죠. 그럼 grade를 읽을 때는 주소에 두 번 접근을 해야 합니다. 따라서 어차피 2바이트 사용한다면 구역을 나눠서 두 번째 시작 위치부터 grade를 할당하는 게 속도 향상에 도움이 되죠.

이때 다음과 같이 경계를 나누는데 사용하는 비트 필드 이름을 생략할 수 있어요.

struct BF{
unsigned int sex: 1;
unsigned int contry: 5;
unsigned int : 0; <= 바이트 경계 변수명 생략
unsigned int grade: 3;
unsigned int room: 4;
};

왜 바이트 나누기를 할까요? 메모리 주소는 바이트 단위로 접근할 수 있다고 했죠? 위에서 구역을 나누지 않으면 grade가 첫 번째 바이트와 두 번째 사이에 걸치게 되죠. 그럼 grade를 읽을 때는 주소에 두 번 접근을 해야 합니다. 따라서 어차피 2바이트 사용한다면 구역을 나눠서 두 번째 시작 위치부터 grade를 할당하는 게 속도 향상에 도움이 되죠.

비트 건너뛰고 할당하기

건너뛸 비트 수를 명시할 수도 있습니다. 비트 필드 변수명 없이 건너뛸 비트 수만 적어주면 됩니다.

struct BF{
unsigned int sex: 1;
unsigned int : 5; <= 건너뛸 비트 수 지정
unsigned int grade: 3;
unsigned int room: 4;
};

자료형 생략하기

비트 필드에서는 다음과 같이 자료형을 생략할 수 있습니다.

struct BF{
unsigned sex: 1;
unsigned boundary: 0;
unsigned grade: 3;
unsigned room: 4;
};

그렇다고 변수명과 비트 크기만 쓰는 방식을 지원하지는 않습니다.

struct BF{
 sex: 1;   // 컴파일 에러 발생
 grade: 3; // 컴파일 에러 발생
 room: 4;  // 컴파일 에러 발생
};

signed는 지정은 무의미해

비트 필드에서도 signed를 사용할 수 있습니다. 하지만 어차피 빅엔디안 방식으로 순차적으로 비트가 할당되므로 큰 의미는 없습니다.

struct BF{
	signed sex: 1;
	signed grade: 3;
	signed room: 4;
};

일반 멤버 변수와 비트 필드 쓰기

그렇다면 일반 멤버 변수화 비트 필드 변수를 같이 쓸 수 있을까요? 물론 같이 쓸 수 있습니다. 사용 법은 기존과 같아서 다를게 없지만, 참고 삼을 수 있게 간단한 예제를 제공해드릴게요.

#include <stdio.h>
 
struct BF{
char name[20];
unsigned int sex: 1;   // 1이면 남성, 0이면 여성
unsigned int grade: 3; // 초등학교는 3비트면 표현할 수 있습니다.
unsigned int room: 4; // 10학급까지 있다면 4비트로 표현할 수 있습니다.
};
 
int main(void) {
 
	struct BF bitfield = {"관우", 1, 2, 3};
 
	if(bitfield.sex){
		printf("남성 : %d학년 %d반", bitfield.grade, bitfield.room);
	}
	else{
		printf("여성 : %d학년 %d반", bitfield.grade, bitfield.room);
	}
 
	return 0;
}

[결과]

남성 : 2학년 3반

비트 필드 변수가 아닌 name이 비트 필드 변수와 같은 구조체에 있습니다. 일반 구조체 때와 같은 방식으로 사용하면 되는군요!

공용체에서 비트 필드 활용하기

공용체에서도 비트 필드를 사용할 수 있습니다.

어떻게?

이름 없는 구조체를 사용해서요!

#include <stdio.h>
 
union BF{
	int age;
	
	struct {
		unsigned int sex: 1;   // 1이면 남성, 0이면 여성
		unsigned int grade: 3; // 초등학교는 3비트면 표현할 수 있습니다.
		unsigned int room: 4; // 10학급까지 있다면 4비트로 표현할 수 있습니다.
		};
};
 
int main(void) {
	
	union BF bitfield;
	
	bitfield.sex = 1;
	bitfield.grade = 2;
	bitfield.room = 3; 
 
	if(bitfield.sex){
		printf("남성 : %d학년 %d반", bitfield.grade, bitfield.room);
	}
	else{
		printf("여성 : %d학년 %d반", bitfield.grade, bitfield.room);
	}
 
	return 0;
}

[결과]

남성 : 2학년 3반

공용체 안의 구조체를 보시면 이름이 없습니다. 이미 공용체를 다루면서 이름 없는 공용체를 구조체 안에 사용한다는 걸 배웠습니다. 그 반대니까 어렵지 않을 겁니다.

이상 관우였습니다.