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바이트 단위로 정렬하게 되므로 남는 공간 없이 자료형 크기 그대로 메모리에 올라갑니다.
#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바이트 단위로 구조체를 정렬한 모양입니다.
만약 이 상태에서 8, 16바이트로 구조체를 정렬하더라도 구조체의 크기는 8이 나올 것입니다. 왜냐하면 구조체 안에서 가장 큰 자료형의 크기(4)보다 정렬 설정 크기(8, 16)가 크기 때문입니다. 보통 #pragma pack(push, 1)을 주로 사용하므로 신경 쓰지 않아도 됩니다.
궁금한 분들은 각자 char, int 대신 크기가 큰 double, long long 멤버를 넣어서 구조체 크기를 확인해보세요.
Visual Studio에서 구조체 정렬의 기본값은 8바이트이며 컴파일 옵션에서 다른 값으로 설정할 수 있습니다. 솔루션 탐색기에서 프로젝트 선택 > 메인 메뉴의 프로젝트(P) > 속성(P) > C/C++ > 코드 생성 > 구조체 멤버 맞춤
- /Zp (구조체 멤버 맞춤): https://msdn.microsoft.com/ko-kr/library/xh3e3fd0.aspx
GCC에서 구조체 정렬 기본값은 int의 크기이며 -fpack-struct 옵션으로 다른 값을 설정할 수 있습니다.
- Options for Code Generation Conventions: https://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html#Code-Gen-Options
다음은 GCC에서 구조체를 1바이트로 정렬합니다.
$ gcc struct_align.c -fpack-struct=1
지금까지 구조체 정렬에 대해 배웠는데 CPU와 메모리에 관련된 부분이라 내용이 좀 어려웠습니다. 당장은 정렬 원리나 내부 동작을 이해하지 않아도 되며 구조체를 1바이트 크기로 정렬하는 것은 구조체의 내용을 파일에 쓰거나 네트워크로 전송할 때 꼭 필요하다는 점만 알아두면 됩니다.