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바이트 크기로 정렬하는 것은 구조체의 내용을 파일에 쓰거나 네트워크로 전송할 때 꼭 필요하다는 점만 알아두면 됩니다.