본문 바로가기
프로그래밍 언어/C, C++

[C/C++] 구조체 비트필드(bit-field)

by 니키티스 2024. 2. 24.

C/C++: 구조체 비트필드

비트필드에 대한 쉽고 간단한 설명을 찾는다면 코딩도장에 잘 정리되어 있으니 먼저 한 번 읽어보는 것을 추천한다.

C 언어 코딩 도장: 56.1 구조체 비트 필드를 만들고 사용하기 (dojang.io)

비트 필드에 대해 찾게 된 이유

언리얼 엔진을 공부하다가 대체 헤더 파일에서 싱글 콜론(:)이 왜 나오는지 궁금했다. 처음에는 언리얼 엔진만의 문법이라 생각했는데 아무리 검색해 봐도 이에 대한 설명이 없었다.

// 멤버 변수에 대한 헤더 파일 코드
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Character)
uint8 bIsIdle : 1;

UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Character)
float MovingThreshold;

UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Character)
uint8 bIsFalling : 1;

UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Character)
uint8 bIsJumping : 1;

UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Character)
float JumpingThreshold;

위는 언리얼 엔진에서 이득우 교수의 강의를 들으며 작성한 클래스 선언 코드인데, 아무리 봐도 uint8 bIsJumping : 1; 뒤의 : 1의 정체를 알 수 없었다.

의외로 이 문법은 C언어에서부터 있었던 것으로, 비트 필드(bit field)를 표현하는 것이다.

C언어에는 클래스가 존재하지 않으므로 구조체에서 사용하여 구조체 비트필드라고도 부른다. C++에서도 동일하게 사용가능한 것으로 보인다.

비트 필드

비트 필드란 구조체에서 정수형 데이터를 비트 단위로 나누어 사용할 수 있는 기능이다.

C99 표준에서는 비트 필드로 사용할 수 있는 자료형을 _Bool(C언어의 boolean type), signed int, unsigned int, int로 규정하고 있다. 다만 대부분의 컴파일러에서는 모든 정수 자료형을 사용할 수 있다. 실수 자료형은 비트 필드로 사용할 수 없다는 점을 주의하자.

struct 구조체이름 {
    정수자료형 멤버이름 : 비트수;
};

보통 비트필드는 저수준(low level) 프로그래밍을 할 때 비트 단위로 구조체를 제어하기 위해 사용된다. 다만 구조체에 대한 이해가 필요해 다소 난이도가 어렵다. 처음 C언어를 접할 때보단 충분히 익숙해지고 나서 공부하도록 하자.

예시 1: 정규 크기를 할당할 때

struct normal_struct
{
    unsigned int a : 1;
    unsigned int b : 2;
    unsigned int c : 5;
};

이렇게 넉넉하게 사이즈를 남기면 오류 없이 정상 실행된다.

struct normal_struct
{
    unsigned int a : 1;
    unsigned int b : 2;
    unsigned int c : 5;
    unsigned int d : 24;
};

이렇게 딱 맞춰서 할당할 수도 있다.

예시 2: 데이터 타입보다 더 많은 비트를 할당할 때

한 번에 자신의 타입 크기보다 더 많은 비트를 할당하려 하면 컴파일러에서는 오류가 발생한다.

struct oversized_struct
{
    unsigned char a : 7;
    unsigned char b : 5;
    unsigned char c : 24; // Error: invalid size for bit fieldC/C++(105)
};

char는 8비트인데 비해 그보다 큰 24비트를 할당하려고 하니 아예 실행이 안 된다.

C99 표준에 따르면 비트필드는 음이 아니고 각 데이터형보다 비트 수가 크면 안 된다.

The expression that specifies the width of a bit-field shall be an integer constant
expression that has nonnegative value that shall not exceed the number of bits in an object
of the type that is specified if the colon and expression are omitted. If the value is zero,
the declaration shall have no declarator.

비트필드의 크기는 자료형의 최대 크기인 64비트이지만, 운영체제 호환을 위해 32 이하의 값을 사용하는 것을 추천한다.

예시 3: 멤버의 총 비트 수가 데이터 타입보다 클 때

그렇다면 멤버의 총 비트 수가 데이터 타입에 정해진 비트수보다 크면 어떻게 될까? unsigned short는 총 16비트인데, 1+2+4+16=23비트로 unsigned short의 크기를 초과한다. 직관적으로는 unsigned short 크기로 하나 더 할당될 것 같은데 어떻게 될까?

#include <stdio.h>
#include <stdbool.h>

struct normal_struct
{
    unsigned int a : 1;
    unsigned int b : 2;
    unsigned int c : 5;
    unsigned int d : 24;
};

struct oversized_struct_short
{
    unsigned short a : 1;
    unsigned short b : 2;
    unsigned short c : 4;
    unsigned short d : 16;
};

int main()
{
    printf("normal: %d\n", sizeof(struct normal_struct));
    printf("oversized: %d\n", sizeof(struct oversized_struct_short));

    return 0;
}

결과는 다음과 같다.

normal: 4
oversized: 4

oversized_struct_short을 보면, a, b, c에서 7비트를 사용하였다. 따라서 d가 들어오게 되면 총 7+16=23비트가 되어 하나의 short 안에 데이터가 저장되지 못한다. 그래서 구조체는 두 개의 short을 사용하게 된다.

대략 메모리 주소를 비트 단위로 나타내면 이런 느낌일 것이다.

a b b c c c c . . . . . . . . .
d d d d d d d d d d d d d d d d

실제로 구조체를 출력해서 확인해 보자.

#include <stdio.h>
#include <string.h>

struct oversized_struct_short
{
    unsigned short a : 1;
    unsigned short b : 2;
    unsigned short c : 5;
    unsigned short d : 16;
};

int main()
{
    struct oversized_struct_short oss;

    memset(&oss, 0, sizeof(oss));

    oss.a = 0;
    oss.b = 0xFF;
    oss.c = 0;
    oss.d = 0xFFFF;

    printf("%04X(size: %d)\n", oss, sizeof(oss));

    return 0;
}
// Output:
// FFFF0006(size: 4)

계산기에 따르면 이와 같다.

0xFFFF 0006를 이진수로 변환한 결과.

값을 다르게 하며 확인해 보면, 각각의 변수는 dddd/dddd/dddd/dddd/…./…./.ccc/cbba 꼴로 출력된다. 구조 상으로는 앞의 표를 뒤집은 것과 같다.

d d d d d d d d d d d d d d d d
. . . . . . . . c c c c c b b a

여기서 특이한 점은, b는 0xFF를 넣었는데 오른쪽 끝에는 11이 저장된 것으로 끝났다. 이는 b의 비트필드를 2로 제한했기 때문이다. 그래서 0xFF에서 오른쪽 끝 두 자리만 남기고 제거되었다.

비트필드를 사용할 때 구조체 내의 바이트 순서는 엔디언에 따라, 컴파일러마다 다르다. C99 표준에서는 구현에 따라 달라진다고 말했다(gcc - Order of fields when using a bit field in C - Stack Overflow Comment 참고).

대부분의 리틀엔디언은 위의 결과처럼 LSB부터 채운다. 그러면 a가 LSB에, b가 LSB 왼쪽으로 2비트에 데이터를 저장하게 되는 식이다.

비트 필드는 주소 연산자를 사용할 수 없다

하지만 이를 주소 값으로 확인하기 위해 주소 연산자(&)를 사용하면 오류가 뜨게 된다. 그 원인은 C언어의 포인터로 구분할 수 있는 최소 단위가 바이트기 때문이다. 따라서 비트 필드는 주소를 확인할 수 없다.

실제 C언어 표준(C11 6.5.3.2 Address and indirection operators)에서도 &는 비트 필드가 아니어야 한다고 명시되어 있다고 한다.

pointers - c - cannot take address of bit-field - Stack Overflow

예시 4: 여러 타입을 사용할 때

struct mixed_struct
{
    unsigned short a : 4;
    signed long b : 16;
};

int main()
{
    printf("mixed: %d\n", sizeof(struct mixed_struct));

    return 0;
}

// -------------------------
// output:
// mixed: 8
// -------------------------

여러 타입을 사용하면 구조체의 크기는 가장 큰 데이터 타입의 크기와 같다.

여기서는 64bit 기반의 메모리 주소를 사용하여 long의 크기가 8바이트이다. short보다 long의 크기가 크므로 구조체의 크기는 long의 크기 8이 된다.

예시 5: 비트필드를 0으로 지정할 때

비트필드의 값 0은 다음 경계 부분에 맞추어 바이트를 정렬할 때 사용된다.

다음은 geeksforgeeks의 코드로, 0으로 지정해서 바이트 정렬을 맞추도록 한다.

// https://www.geeksforgeeks.org/bit-fields-c/
#include <stdio.h>

// A structure without forced alignment
struct test1 {
    unsigned int x : 5;
    unsigned int y : 8;
};

// A structure with forced alignment
struct test2 {
    unsigned int x : 5;
    unsigned int : 0;
    unsigned int y : 8;
};

int main()
{
    printf("Size of test1 is %lu bytes\n",
           sizeof(struct test1));
    printf("Size of test2 is %lu bytes\n",
           sizeof(struct test2));
    return 0;
}
// Output
// Size of test1 is 4 bytes
// Size of test2 is 8 bytes

test1은 하나의 int 안에 들어가서 구조체 크기가 4바이트가 되지만, test2는 unsigned int : 0; 부분에서 다음 경계로 위치를 맞추게 된다. 따라서 y는 x의 시작지점에서 5비트 뒤(x의 비트필드)가 아니라 32비트 뒤(int의 크기)에 위치하게 된다.

이를 이용하면 원하는 대로 구조체의 바이트 정렬 방식을 바꾸는 것도 가능하다.

약간의 문제점

흔히 사람들이 많이 사용하는 컴파일러 GCC는 실제 C11 표준과 동작 방식이 다르다는 말이 있다.

그 때문에 실제 C11 표준에 따라 코드를 짜면 예상한 결과가 나오지 않을 수도 있다고 하니 주의하자. 자세히 알고 싶다면 다음 글을 읽어보는 것을 추천한다.

c - Bit-field types in GCC vs C11 standard - Stack Overflow

출처

C 언어 코딩 도장: 56.1 구조체 비트 필드를 만들고 사용하기 (dojang.io)

https://www.it-note.kr/312

Bit Fields in C - GeeksforGeeks

c - What is the type of a bitfield? - Stack Overflow

[Programming/C] 비트 단위로 제어하기 (Struct Bit Field 활용) (tistory.com)

댓글