구조체 메모리 크기와 정렬, 패딩 (Struct Alignment)
한눈에 보기
구조체 크기가 멤버 합보다 커지는 이유인 메모리 정렬과 패딩의 규칙을 오프셋 계산 예시로 설명하고, 멤버 순서에 따른 크기 차이와 #pragma pack 사용 시 주의점을 다룬다.
최종 업데이트 2026.09.17
구조체(Struct)의 크기는 멤버 크기의 단순 합이 아니라, 컴파일러가 메모리 정렬(Alignment)을 맞추기 위해 삽입하는 패딩(Padding)까지 더한 값이다. CPU가 데이터를 효율적으로 읽으려면 각 타입이 자신의 크기 배수 주소에 놓여야 하기 때문에, 멤버 선언 순서에 따라 같은 멤버 구성이라도 구조체 크기가 달라진다. 이 원리를 알면 sizeof 결과를 정확히 예측하고 메모리를 아끼는 멤버 배치를 설계할 수 있다.
메모리 정렬이 필요한 이유
CPU는 메모리를 1바이트씩이 아니라 워드(Word) 단위로 읽는다. 4바이트 int가 4의 배수 주소에 있으면 한 번의 접근으로 읽을 수 있지만, 어중간한 주소에 걸쳐 있으면 두 번 읽어서 조합해야 하거나, 일부 아키텍처에서는 아예 정렬 오류로 프로그램이 중단된다. 그래서 각 타입에는 정렬 요구사항(Alignment Requirement)이 있으며 보통 타입 크기와 같다. char는 1, short는 2, int와 float는 4, double과 64비트 환경의 포인터는 8이다. 컴파일러는 이 요구를 만족시키기 위해 멤버 사이에 빈 공간을 끼워 넣는데 이것이 패딩이다.
패딩이 삽입되는 규칙
규칙은 두 가지로 정리된다. 첫째, 각 멤버는 자신의 정렬 요구사항의 배수 오프셋(Offset)에 배치되며, 그 앞에 필요한 만큼 내부 패딩이 들어간다. 둘째, 구조체 전체 크기는 멤버 중 가장 큰 정렬 요구사항의 배수가 되도록 끝에 후행 패딩이 붙는다. 후행 패딩은 구조체 배열을 만들었을 때 두 번째 원소부터도 정렬이 유지되도록 하기 위한 것이다.
#include <stdio.h>
struct A { /* 총 12바이트 */
char c; /* 오프셋 0 */
/* 패딩 3바이트 */
int i; /* 오프셋 4 */
char d; /* 오프셋 8 */
/* 후행 패딩 3바이트 */
};
struct B { /* 총 8바이트 */
int i; /* 오프셋 0 */
char c; /* 오프셋 4 */
char d; /* 오프셋 5 */
/* 후행 패딩 2바이트 */
};
int main(void) {
printf("%zu %zu\n", sizeof(struct A), sizeof(struct B));
return 0;
}
두 구조체는 int 하나와 char 두 개라는 같은 멤버를 가지지만, 선언 순서 때문에 A는 12바이트, B는 8바이트가 된다. 큰 타입부터 작은 타입 순으로 배치하면 패딩이 줄어드는 경향이 있으며, 대량으로 배열에 담기는 구조체라면 이 차이가 전체 메모리 사용량과 캐시(Cache) 효율에 직접 영향을 준다.
크기 계산 연습
| 구조체 멤버 | 배치(64비트 기준) | sizeof |
|---|---|---|
| char, double, int | 0, 패딩 7, 8, 16, 후행 패딩 4 | 24 |
| double, int, char | 0, 8, 12, 후행 패딩 3 | 16 |
| char, char, short, int | 0, 1, 2, 4 | 8 |
| char만 3개 | 0, 1, 2 | 3 |
가장 큰 멤버가 double이면 전체 크기는 8의 배수, int가 최대면 4의 배수가 된다. 멤버가 char뿐이면 정렬 요구사항이 1이므로 패딩이 전혀 없다. 특정 멤버의 오프셋은 stddef.h의 offsetof 매크로로 확인할 수 있고, C11부터는 _Alignof 연산자(C++에서는 alignof)로 타입의 정렬 요구사항 자체를 조회할 수 있다.
패딩 제어와 주의할 점
네트워크 패킷 헤더나 파일 포맷처럼 바이트 배치가 규격으로 고정된 데이터를 다룰 때는 패딩이 방해가 된다. 이때 #pragma pack(1) 같은 지시문으로 패딩을 없앨 수 있지만, 정렬되지 않은 접근으로 인한 성능 저하나 일부 플랫폼에서의 오류를 감수해야 하므로 꼭 필요한 구간에만 제한적으로 쓴다. 패딩 바이트는 값이 보장되지 않는 쓰레기 공간이므로 memcmp로 구조체 전체를 비교하면 논리적으로 같은 두 구조체가 다르다고 판정될 수 있다는 점도 주의해야 한다. 구조체를 그대로 파일이나 소켓에 write하는 코드가 플랫폼 간에 호환되지 않는 이유도, 컴파일러와 아키텍처마다 패딩 배치가 다를 수 있기 때문이다. 직렬화(Serialization)는 멤버 단위로 명시적으로 하는 것이 안전하다.
C++에서도 같은 규칙이 적용되며, 가상 함수가 있는 클래스는 가상 함수 테이블 포인터(vptr)가 숨은 멤버로 추가되어 크기가 포인터 하나만큼 더 커진다. 또한 멤버가 하나도 없는 빈 구조체도 크기가 0이 아니라 1인데, 서로 다른 객체가 서로 다른 주소를 가져야 한다는 규칙 때문이다.
면접에서 이렇게 물어본다
- char, int, char 순서로 멤버를 가진 구조체의 크기를 계산 과정과 함께 설명해 보라.
- 패딩은 왜 존재하며, CPU 입장에서 정렬된 접근이 왜 유리한가?
- 멤버 순서만 바꿔서 구조체 크기를 줄일 수 있는 이유는 무엇인가?
- 구조체 전체 크기가 가장 큰 멤버의 정렬 요구사항 배수가 되어야 하는 이유를 배열 관점에서 설명해 보라.
- #pragma pack(1)은 언제 쓰며 어떤 부작용이 있는가?
C / C++에서 같이 읽기
- C 언어 컴파일 과정 (Compile Process)C 소스 코드가 전처리, 컴파일, 어셈블, 링킹 네 단계를 거쳐 실행 파일이 되는 과정을 단계별 산출물, 목적 파일의 섹션 구조, 정적 링킹과 동적 링킹의 차이와 함께 정리한다.
- C 동적 할당 (malloc, free)실행 중에 힙 메모리를 확보하고 반납하는 C의 동적 할당을 malloc, calloc, realloc, free의 동작과 할당기의 내부 구조, 메모리 누수와 댕글링 포인터 같은 대표 오류 중심으로 설명한다.
- 포인터 (Pointer)다른 변수의 메모리 주소를 저장하는 포인터의 개념과 역참조, 포인터 산술, 배열과의 관계, 이중 포인터와 함수 포인터, 그리고 널 포인터와 댕글링 포인터 같은 오류 상황까지 정리한다.
- C++ 입출력 성능 최적화 (I/O Optimization)cin과 cout이 기본 설정에서 느린 이유를 stdio 동기화와 버퍼 플러시로 설명하고, sync_with_stdio(false), cin.tie(NULL), endl 대신 개행 문자를 쓰는 세 가지 최적화와 주의점을 정리한다.
이 개념, 내 코드에서는 어떻게 물어볼까요
데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.
공고 넣고 시작하기