POCU C언어 정주행 11회차 - 비트 패턴, 공용체

2022. 12. 26. 20:36C언어 복습

1. 비트 패턴

C언어를 배운 사람들 중에서 비트 패턴을 기억하는 사람은 거의 없을 것이라고 생각한다. 왜냐? 공용체와 마찬가지인데 거의 안쓰기 때문이다... 하지만 기왕 모르는 것들 정리하기로 한 김에 이것도 같이 정리하려고 한다. 근데 솔직히 말하면 거의 안쓰는 거여서 그런지 강의에서도 그렇게 깊게 다룬 것 같지는 않았다. 그래서 그냥 나도 강의에서 다룬 것까지만 정리하고 넘어가려고 한다.

 

비트 패턴이 뭘까? 구조체를 배울 때 여러 종류의 데이터들을 하나의 자료형으로 묶어서 관리했던 것을 기억할 것이다. 근데 그런 자료형들은 기본 자료형들로 구성되어 있을 것이며 8비트, 16비트, 32비트 등으로 정해져있다. 즉, 어떤 데이터를 저장하거나 표현하는 데 있어서 8비트, 16비트, 32비트와 같이 정해진 크기를 사용해서 저장해야 했다는 뜻이다. 그러나 비트 필드를 사용하면 이런 단점을 극복할 수 있다. 다음 코드가 비트 패턴을 사용하는 대표적인 예시이다.

typedef struct
{
    unsigned char b0 : 1;
    unsigned char b1 : 1;
    unsigned char b2 : 1;
    unsigned char b3 : 1;
    unsigned char b4 : 1;
    unsigned char b5 : 1;
    unsigned char b6 : 1;
    unsigned char b7 : 1;
} Foo;

이렇게 되면 b0~b7을 거쳐서 총 8비트를 사용하게 되며 각각의 비트에 구조체 멤버 접근을 하는 것처럼 접근해서 사용하면 된다. unsigned char라는 8비트짜리 자료형을 각각 1비트씩 나눠서 사용하고 있는 모습이며 만약 뒤에 있는 1이 다른 숫자라면 그 숫자에 해당하는 만큼의 비트를 사용하게 된다. 즉, 저런 방식으로 8개의 bool형 데이터를 표현하기 위해서 1비트씩 할당하는 방식도 가능하며 지금같은 경우는 31바이트의 메모리를 절약할 수 있는 것이다.

 

물론 당연히 아래와 같이 코드를 짤 수도 있다.

typedef struct
{
    char c0 : 4;
    char c1 : 4;
    int i0 : 8;
    int i1 : 8;
    int i2 : 8;
} Foo;

이러면 char 자료형을 기준으로 상위 4비트를 c0, 하위 4비트를 c1이 사용한다. int의 경우에는 i0, i1, i2가 각각 int의 32비트 중 8비트씩 사용하게 되며 Foo의 전체 메모리는 이전의 글대로 총 8바이트를 차지하게 된다. 이렇게 되면 i0, i1, i2이외의 나머지 8비트는 사용되지 않는 공간으로 낭비되는 것이다.

 

비트 패턴을 사용한다면 비트 패턴으로 선언한 것을 한꺼번에 사용하고 싶을 때가 있을 수 있다. 근데 그렇다고 아래와 같은 코드를 짜면 컴파일 오류가 발생하기 때문에 주의해야 한다. 어쨋든 구조체로 판정이 들어가기 때문에 아래와 같이 비교 연산을 하려고 한다고 가정하면 컴파일 오류가 발생하는 것이다.

int is_set = (flags.b1 == 1);            /* OK */
int is_same = (flags.b1 == flags.b7);    /* OK */
int is_all = (flags == 0xFF);            /* 컴파일 오류 */
int is_zero = (flags == 0);              /* 컴파일 오류 */

 

위와 같은 문제를 해결하기 위해 아래와 같이 형변환을 사용할 수 있다. 하지만 이는 매우 위험한 것으로 실수가 나올 확률이 매우 높다. 강의에서는 이 문제를 해결할 수 있는 방법으로 공용체를 소개했다.

char* val;
int is_zero;
bitflags_t flags = { 0 };

flags.b3 = 1;

val = (char*)&flags;
is_zero = (*val == 0);

 

 

2. 공용체

공용체도 솔직히 말해서 비트 패턴과 마찬가지로 거의 안쓴다... 그래서 개인적으로 그냥 안배우고 넘어가도 나중에 업계에서 일할 때 거의 문제가 없을지도 모른다고 생각한다. 근데 난 그래도 이 내용을 정리하려고 한다.

 

공용체는 같은 메모리 주소를 다른 자료형으로 읽고 싶을 때 사용한다. 비트 패턴을 소개할 때 같이 소개한 예제를 기억하면서 다음 예시를 보자.

typedef union
{
    unsigned char val;
    struct
    {
        unsigned char b0 : 1;
        unsigned char b1 : 1;
        unsigned char b2 : 1;
        unsigned char b3 : 1;
        unsigned char b4 : 1;
        unsigned char b5 : 1;
        unsigned char b6 : 1;
        unsigned char b7 : 1;
    } bits;
} bitflags_t;

int is_same;
int is_zero;
bitflags_t flags = { 0 };

flags.bits.b1 = 1;
flags.bits.b4 = 1;

is_same = (flags.bits.b1 == flags.bits.b7);
is_zero = (flags.val == 0);

아까랑 마찬가지로 8개의 bool형 데이터를 위해서 메모리 절약을 위해 비트 패턴을 사용한 상황이다. 여기서 모든 비트가 0으로 세팅이 되어있는지 보기 위해서 이전 예제에서는 강제로 다른 자료형으로 형변환을 시켰지만 여기서는 그럴 필요없이 val을 이용해서 8개의 비트가 모두 0으로 세팅되어 있는지 확인할 수 있다. 다른 예시를 한번 보자.

#include <stdio.h>

typedef union
{
    unsigned int val;
    
    struct
    {
        unsigned char r;
        unsigned char g;
        unsigned char b;
        unsigned char a;
    } rgba;
} color_t;

int main(void)
{
    color_t trans_black;
    color_t red;
    color_t yellow;
    
    trans_black.val = 0;
    
    red.val = 0;
    red.rgba.r = 255;
    red.rgba.a = 255;
    
    yellow = red;
    yellow.rgba.g = 255;
    
    printf("size: %d\n", sizeof(color_t));
    printf(" black: 0x%08x(%3d, %3d, %3d, %3d)\n",
        trans_black.val,
        trans_black.rgba.r, trans_black.rgba.g,
        trans_black.rgba.b, trans_black.rgba.a);
        
    printf("   red: 0x%08x(%3d, %3d, %3d, %3d)\n",
        red.val,
        red.rgba.r, red.rgba.g,
        red.rgba.b, red.rgba.a);
        
    printf("yellow: 0x%08x(%3d, %3d, %3d, %3d)\n",
        yellow.val,
        yellow.rgba.r, yellow.rgba.g,
        yellow.rgba.b, yellow.rgba.a);
        
    return 0;
}

0~255의 rgb값으로 색상을 표현하기 위해 unsigned char로 이루어진 구조체를 만들었고 구조체의 각 항목에 해당하는 값들을 한꺼번에 가져오기 위해서 공용체를 이용했고 unsigned int val; 을 선언한 것을 볼 수 있다. 이렇게 하면 val을 통해서 rgba의 값을 한꺼번에 읽어올 수 있으며 실제로 색상을 표현하는 코드를 짤 때 이 방법을 자주 쓴다고 한다.