51.2 구조체 정렬 크기 조절하기

데이터 전송이나 저장을 할 때 구조체 정렬을 피하려면 어떻게 해야 할까요? 그런데 C 언어에서는 구조체를 정렬하는 표준 방법이 없습니다.

하지만 걱정하지 않아도 됩니다. 각 컴파일러에서 제공하는 특별한 지시자를 사용하면 구조체 정렬 크기를 조절할 수 있습니다.

  • Visual Studio, GCC 4.0 이상
#pragma pack(push, 정렬크기)
#pragma pack(pop)
  • GCC 4.0 미만
__attribute__((aligned(정렬크기), packed))

다음 내용을 소스 코드 편집 창에 입력한 뒤 실행해보세요.

struct_align.c

#include <stdio.h>

#pragma pack(push, 1)    // 1바이트 크기로 정렬
struct PacketHeader {
    char flags;    // 1바이트
    int seq;       // 4바이트
};
#pragma pack(pop)        // 정렬 설정을 이전 상태(기본값)로 되돌림

int main()
{
    struct PacketHeader header;

    printf("%d\n", sizeof(header.flags));    // 1: char는 1바이트
    printf("%d\n", sizeof(header.seq));      // 4: int는 4바이트
    printf("%d\n", sizeof(header));          // 5: 1바이트 단위로 정렬했으므로 
                                             // 구조체 전체 크기는 5바이트

    return 0;
}

실행 결과

1
4
5

구조체를 정의할 때 위 아래로 #pragma pack(push, 1)과 #pragma pack(pop)를 넣어주었습니다. 여기서 중요한 부분은 pack 안의 1입니다. C 언어에서 자료형의 크기는 바이트 단위이고 가장 작은 크기는 1바이트죠. 따라서 pack을 1로 설정하면 1바이트 단위로 정렬하게 되므로 남는 공간 없이 자료형 크기 그대로 메모리에 올라갑니다.

▼ 그림 51‑2 1바이트 크기로 구조체 정렬

#pragma pack(push, 1)을 한 번 사용하면 그 아래에 오는 모든 구조체에 영향을 주므로 정렬 설정을 한 뒤에는 #pragma pack(pop)를 사용하여 설정을 이전 상태로 되돌립니다.

만약 GCC 버전이 4.0 미만이라면 #pragma pack(push, 1), #pragma pack(pop) 대신 __attribute__((aligned(1), packed))를 사용합니다.

struct_align_gcc.c

#include <stdio.h>

struct PacketHeader {
    char flags;    // 1바이트
    int seq;    // 4바이트
} __attribute__((aligned(1), packed));    // GCC 4.0 미만: 1바이트 크기로 정렬

int main()
{
    struct PacketHeader header;

    printf("%d\n", sizeof(header.flags));     // 1: char는 1바이트
    printf("%d\n", sizeof(header.seq));       // 4: int는 4바이트
    printf("%d\n", sizeof(header));           // 5: 1바이트 단위로 정렬했으므로 
                                              // 구조체 전체 크기는 5바이트

    return 0;
}

실행 결과

1
4
5

이제 정말 멤버의 위치가 그림 51-2처럼 되었는지 확인해보겠습니다.

struct_align_offsetof.c

#include <stdio.h>
#include <stddef.h>   // offsetof 매크로가 정의된 헤더 파일

#pragma pack(push, 1)    // 1바이트 크기로 정렬
struct PacketHeader {
    char flags;    // 1바이트
    int seq;       // 4바이트
};
#pragma pack(pop)        // 정렬 설정을 이전 상태(기본값)로 되돌림

int main()
{
    printf("%d\n", offsetof(struct PacketHeader, flags));    // 0
    printf("%d\n", offsetof(struct PacketHeader, seq));      // 1

    return 0;
}

실행 결과

0
1

구조체를 1바이트 단위로 정렬했으므로 seq의 상대 위치는 1이 나옵니다. 즉, 자료형 크기 그대로 이기 때문에 char flags 바로 뒤에 int seq가 와서 1입니다.

#pragma pack(push, 1), __attribute__((aligned(1),packed))에는 1 이외에도 2, 4, 8, 16도 지정할 수 있습니다. 다음 그림은 2바이트와 4바이트 단위로 구조체를 정렬한 모양입니다.

▼ 그림 51‑3 2, 4바이트 크기로 구조체 정렬

만약 이 상태에서 8, 16바이트로 구조체를 정렬하더라도 구조체의 크기는 8이 나올 것입니다. 왜냐하면 구조체 안에서 가장 큰 자료형의 크기(4)보다 정렬 설정 크기(8, 16)가 크기 때문입니다. 보통 #pragma pack(push, 1)을 주로 사용하므로 신경 쓰지 않아도 됩니다.

궁금한 분들은 각자 char, int 대신 크기가 큰 double, long long 멤버를 넣어서 구조체 크기를 확인해보세요.

참고 | 구조체 정렬 기본값

Visual Studio에서 구조체 정렬의 기본값은 8바이트이며 컴파일 옵션에서 다른 값으로 설정할 수 있습니다. 솔루션 탐색기에서 프로젝트 선택 > 메인 메뉴의 프로젝트(P) > 속성(P) > C/C++ > 코드 생성 > 구조체 멤버 맞춤

GCC에서 구조체 정렬 기본값은 int의 크기이며 -fpack-struct 옵션으로 다른 값을 설정할 수 있습니다.

다음은 GCC에서 구조체를 1바이트로 정렬합니다.

$ gcc struct_align.c -fpack-struct=1

지금까지 구조체 정렬에 대해 배웠는데 CPU와 메모리에 관련된 부분이라 내용이 좀 어려웠습니다. 당장은 정렬 원리나 내부 동작을 이해하지 않아도 되며 구조체를 1바이트 크기로 정렬하는 것은 구조체의 내용을 파일에 쓰거나 네트워크로 전송할 때 꼭 필요하다는 점만 알아두면 됩니다.