C · 기본
메모리 그림으로 배우는 C
C 구조체와 메모리 배치 - offsetof 패딩 정렬과 필드 순서 (C 기초 10장)
구조체의 필드 오프셋과 패딩을 offsetof와 -Wpadded로 측정하고, 필드 순서로 크기를 줄이며, 구조체 복사와 포인터 전달의 차이를 본다.
개발자 · 원고 갱신
이 장에서 배우는 것
9장의 IntList처럼 포인터·길이·용량을 한 덩어리로 묶으면 함수에 넘기기도, 이해하기도 쉽다. 이 덩어리가 구조체다. 그런데 필드 크기를 더한 값과 sizeof가 다른 경우가 많다. CPU가 값을 빠르게 읽을 수 있도록 컴파일러가 필드 사이에 빈 바이트, 곧 패딩을 넣기 때문이다. 센서 레코드 수만 개를 메모리에 올리거나 구조체를 그대로 통신·파일에 쓰는 코드에서는 이 빈 바이트가 용량과 호환성 문제로 바로 이어진다.
- 구조체 선언·지정 초기화·대입(복사)·포인터를 통한 필드 접근(
->)을 익힌다. offsetof,sizeof,_Alignof로 필드 배치와 패딩을 측정한다.- 필드 순서를 바꿔 구조체 크기를 줄인다.
-Wpadded로 컴파일러가 넣은 패딩을 확인한다.
문제 상황
센서 한 번 측정값을 uint8_t ID, double 온도, uint16_t 플래그로 담는 구조체를 만들었다. 필드 크기를 더하면 1 + 8 + 2 = 11바이트인데, 측정값 100개 배열이 2,400바이트, 즉 하나에 24바이트를 차지한다. 메모리가 64KB뿐인 보드에서 이 차이는 작지 않다. 또 이 구조체를 그대로 파일에 쓰고 다른 장비에서 읽었더니 값이 어긋난다.
메모리 그림: 정렬과 패딩
정렬(alignment): 4바이트 int 는 4의 배수 주소에, 8바이트 double 은 8의 배수 주소에 놓는다
(이 기계 기준. 대부분의 64비트 환경이 같다)
struct Loose { char kind; int32_t value; char unit; }; sizeof = 12
오프셋 0 1 2 3 4 5 6 7 8 9 10 11
┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐
│kind│ ░░ │ ░░ │ ░░ │ value │unit│ ░░ │ ░░ │ ░░ │
└────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘
└ value 를 4의 배수에 맞추려는 패딩 ┘ └ 배열로 이어 놓을 때
다음 원소를 맞추려는 꼬리 패딩 ┘
struct Ordered { int32_t value; char kind; char unit; }; sizeof = 8
┌────┬────┬────┬────┬────┬────┬────┬────┐
│ value │kind│unit│ ░░ │ ░░ │
└────┴────┴────┴────┴────┴────┴────┴────┘
struct Reading { uint8_t sensor_id; double celsius; uint16_t flags; }; sizeof = 24
0 1 ─────── 7 8 ──────────── 15 16 17 18 ────── 23
[id] [ 패딩 7바이트 ] [ celsius ] [flags ] [ 패딩 6바이트 ]
실제 데이터 11바이트, 빈 바이트 13바이트
규칙은 두 가지다. ① 각 필드는 자기 정렬 단위의 배수 오프셋에 놓인다. ② 구조체 전체 크기는 가장 큰 정렬 단위의 배수가 된다(배열로 늘어놓아도 각 원소가 정렬을 지키도록). 필드 순서는 선언한 순서 그대로이며, 컴파일러가 C 구조체의 필드 순서를 바꾸는 일은 없다. 그래서 순서를 바꾸는 것은 프로그래머의 몫이다.
char, int32_t, char 순서는 패딩 6바이트가 끼어 12바이트지만, int32_t 를 앞으로 옮기면 8바이트가 된다.
uint8_t, double, uint16_t 순서의 Reading 은 데이터 11바이트에 패딩 13바이트로 24바이트를 쓰고, double 을 앞에 두면 16바이트로 줄어든다.
완성 코드
배치 측정
#include <stdio.h>
#include <stddef.h>
#include <stdint.h>
struct Loose {
char kind;
int32_t value;
char unit;
};
struct Ordered {
int32_t value;
char kind;
char unit;
};
struct Reading {
uint8_t sensor_id;
double celsius;
uint16_t flags;
};
#define SHOW(type, member) \
printf(" %-10s offset %2zu, size %zu\n", #member, \
offsetof(type, member), sizeof(((type *)0)->member))
int main(void) {
printf("struct Loose sizeof=%zu alignof=%zu\n", sizeof(struct Loose), _Alignof(struct Loose));
SHOW(struct Loose, kind);
SHOW(struct Loose, value);
SHOW(struct Loose, unit);
printf("struct Ordered sizeof=%zu alignof=%zu\n", sizeof(struct Ordered), _Alignof(struct Ordered));
SHOW(struct Ordered, value);
SHOW(struct Ordered, kind);
SHOW(struct Ordered, unit);
printf("struct Reading sizeof=%zu alignof=%zu\n", sizeof(struct Reading), _Alignof(struct Reading));
SHOW(struct Reading, sensor_id);
SHOW(struct Reading, celsius);
SHOW(struct Reading, flags);
struct Reading log[100];
printf("Reading 100개 배열 = %zu바이트\n", sizeof log);
return 0;
}
구조체 사용: 복사와 포인터
#include <stdio.h>
#include <string.h>
typedef struct {
char title[32];
int price;
int stock;
} Book;
static void discount(Book *b, int percent) {
b->price = b->price * (100 - percent) / 100;
}
static int inventory_value(const Book *books, size_t count) {
int total = 0;
for (size_t i = 0; i < count; i++) {
total += books[i].price * books[i].stock;
}
return total;
}
int main(void) {
Book shelf[] = {
{.title = "C 입문", .price = 20000, .stock = 3},
{.title = "임베디드 기초", .price = 30000, .stock = 1},
};
size_t count = sizeof shelf / sizeof shelf[0];
Book copy = shelf[0];
strcpy(copy.title, "C 입문 (복사본)");
discount(&shelf[0], 10);
printf("shelf[0]: %s %d원\n", shelf[0].title, shelf[0].price);
printf("copy : %s %d원\n", copy.title, copy.price);
printf("재고 금액 합계: %d원\n", inventory_value(shelf, count));
printf("sizeof(Book) = %zu\n", sizeof(Book));
return 0;
}
줄별 해설
offsetof(type, member)—<stddef.h>의 매크로로, 구조체 시작에서 필드까지의 바이트 수를 컴파일 시점에 알려 준다.sizeof(((type *)0)->member)— 필드 하나의 크기다.sizeof는 식을 실제로 계산하지 않으므로 NULL 포인터를 따라가는 것처럼 보여도 안전하다.#define SHOW(type, member) ...의#member— 전처리기의 문자열화 연산자다. 필드 이름을 그대로 문자열로 찍는다._Alignof(struct Reading)— C11부터 있는 키워드로 정렬 단위를 알려 준다.<stdalign.h>를 넣으면alignof로 쓸 수 있다.typedef struct { ... } Book;— 매번struct를 쓰지 않도록 별명을 붙인다.{.title = "C 입문", .price = 20000, .stock = 3}— 지정 초기화(C99). 필드 이름을 적으므로 순서를 바꾸거나 필드를 추가해도 안전하고, 적지 않은 필드는 0이 된다.Book copy = shelf[0];— 구조체 대입은 모든 바이트를 복사한다. 안에 든char title[32]배열까지 통째로 복사되므로copy.title을 바꿔도 원본은 그대로다. 단, 필드가 포인터라면 주소만 복사되어 두 구조체가 같은 힙 블록을 가리키게 된다(얕은 복사).static void discount(Book *b, int percent)— 큰 구조체는 포인터로 넘긴다. 값으로 넘기면 40바이트를 매번 복사하고, 함수 안에서 바꾼 값이 원본에 반영되지 않는다.b->price—(*b).price의 줄임 표기다.const Book *books— 읽기만 하는 함수라는 표시다. 8장의const규칙 그대로다.
실제 실행 결과
$ ./layout
struct Loose sizeof=12 alignof=4
kind offset 0, size 1
value offset 4, size 4
unit offset 8, size 1
struct Ordered sizeof=8 alignof=4
value offset 0, size 4
kind offset 4, size 1
unit offset 5, size 1
struct Reading sizeof=24 alignof=8
sensor_id offset 0, size 1
celsius offset 8, size 8
flags offset 16, size 2
Reading 100개 배열 = 2400바이트
| 구조체 | 필드 | 오프셋 | 크기 |
|---|---|---|---|
| Loose (12바이트) | kind | 0 | 1 |
| value | 4 | 4 | |
| unit | 8 | 1 | |
| Ordered (8바이트) | value | 0 | 4 |
| kind | 4 | 1 | |
| unit | 5 | 1 | |
| Reading (24바이트) | sensor_id | 0 | 1 |
| celsius | 8 | 8 | |
| flags | 16 | 2 |
그림과 같다. Loose와 Ordered는 필드가 같은데 순서만 바꿔 12바이트가 8바이트가 됐다. Reading은 11바이트의 데이터에 24바이트를 쓴다. 이 수치는 이 기계(arm64 macOS) 기준이고, x86-64 리눅스에서도 같게 나오지만 32비트 x86 리눅스에서는 double의 구조체 내 정렬이 4바이트라 Reading이 16바이트가 된다. 8비트 AVR 같은 마이크로컨트롤러는 패딩이 거의 없다. 그래서 구조체 크기를 파일·통신 형식의 약속으로 삼으면 안 된다.
컴파일러에게 패딩을 보고받기
$ clang -std=c17 -Wall -Wextra -Wpadded -c layout.c -o /dev/null
layout.c:19:12: warning: padding struct 'struct Reading' with 7 bytes to align 'celsius' [-Wpadded]
19 | double celsius;
| ^
layout.c:17:8: warning: padding size of 'struct Reading' with 6 bytes to alignment boundary [-Wpadded]
17 | struct Reading {
| ^
layout.c:7:13: warning: padding struct 'struct Loose' with 3 bytes to align 'value' [-Wpadded]
7 | int32_t value;
| ^
layout.c:5:8: warning: padding size of 'struct Loose' with 3 bytes to alignment boundary [-Wpadded]
5 | struct Loose {
| ^
layout.c:11:8: warning: padding size of 'struct Ordered' with 2 bytes to alignment boundary [-Wpadded]
11 | struct Ordered {
| ^
5 warnings generated.
-Wpadded는 패딩이 들어간 곳마다 알려 준다. 모든 구조체에 켜 두면 잡음이 많으니, 메모리 배치가 중요한 헤더를 점검할 때 잠깐 켜는 용도로 쓴다.
구조체 복사와 포인터 전달
$ ./book
shelf[0]: C 입문 18000원
copy : C 입문 (복사본) 20000원
재고 금액 합계: 84000원
sizeof(Book) = 40
discount는 포인터로 원본을 바꿨고, 미리 복사해 둔 copy는 할인 전 가격 20000원을 유지한다. 재고 금액 합계는 18000 × 3 + 30000 × 1 = 84000원이다.
한눈에 보기
| 방법 | 복사하는 양 | 원본 변경 | 어울리는 곳 |
|---|---|---|---|
값 Book b | 구조체 전체 | 불가 | 작은 구조체, 복사본이 필요할 때 |
포인터 Book *b | 주소 8바이트 | 가능 | 원본을 고치는 함수 |
const 포인터 const Book *b | 주소 8바이트 | 불가(컴파일러가 막음) | 읽기만 하는 큰 구조체 |
실무에서 자주 틀리는 것
fwrite(&rec, sizeof rec, 1, fp)로 구조체를 통째로 저장·전송한다. 패딩 바이트(쓰레기 값일 수 있음), 엔디언, 자료형 크기가 모두 플랫폼 의존이라 다른 장비·컴파일러에서 읽으면 어긋난다. 11장에서 이 문제를 직접 보고, 필드별로 바이트 순서를 정해 쓰는 방법을 다룬다.- 패딩을 없애려고 무조건
__attribute__((packed))를 붙인다. 표준이 아닌 컴파일러 확장이고, 정렬이 어긋난 필드를 읽으면 일부 CPU에서는 느려지거나 예외가 난다. 먼저 필드를 큰 것부터 작은 것 순으로 정렬해 본다. - 구조체를
==로 비교하거나memcmp로 비교한다.==는 구조체에 쓸 수 없고,memcmp는 패딩의 쓰레기 값까지 비교해 같은 값도 다르다고 할 수 있다. 필드별로 비교한다. - 포인터 필드가 있는 구조체를 대입으로 복사하고 둘 다
free한다. 얕은 복사라 같은 블록을 두 번 해제하게 된다(9장의 이중 해제).
연습 문제
struct Reading의 필드 순서를 바꿔 크기를 줄여라. 최소 몇 바이트가 되는가?struct P { char a; double b; char c; int d; };의 이 기계에서의sizeof를 오프셋 그림으로 구하라.typedef struct { char *name; int age; } Person;에서Person q = p;뒤q.name[0] = 'X';를 하면p.name은 어떻게 되는가?
정답
double celsius; uint16_t flags; uint8_t sensor_id;순서로 바꾸면 오프셋 0, 8, 10에 놓이고 데이터 끝은 11이다. 전체는 8의 배수여야 하므로 16바이트다. 100개 배열이면 2,400바이트가 1,600바이트로 줄어든다.double이 있는 한 8의 배수 아래로는 줄일 수 없다.a는 0,b는 8의 배수인 8(패딩 7),c는 16,d는 4의 배수인 20(패딩 3)에 놓여 데이터 끝이 24다. 24는 8의 배수이므로 24바이트다. 순서를double b; int d; char a; char c;로 바꾸면 0, 8, 12, 13으로 끝이 14이고, 8의 배수로 올려 16바이트가 된다.p.name도"X..."로 바뀐다. 대입은 포인터 값(주소)만 복사하므로p.name과q.name이 같은 문자열 버퍼를 가리킨다. 독립된 복사본이 필요하면 9장의copy_string처럼 새 블록을 할당해 내용을 복사하는 "깊은 복사" 함수를 만든다.
다음 장에서는 메모리 밖, 파일로 데이터를 옮긴다. 텍스트로 쓸 때와 바이트 그대로 쓸 때의 차이를 이 장의 패딩·엔디언 지식으로 해석한다.
READER FEEDBACK
질문·오탈자·의견
내용에 관한 질문이나 오탈자, 더 나은 설명을 위한 의견을 남겨 주세요. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.