C 언어 시리즈 #07 — 구조체·공용체·비트필드·패딩과 ABI
이 글의 핵심
왜 컴파일러가 패딩을 넣는지, offsetof가 왜 필요한지, union으로 타입을 바꿔 읽기가 왜 엄격 별칭과 충돌하는지, 비트필드가 endian·정렬에 어떻게 묶이는지 정리합니다.
시리즈 안내
지난주에 학생이 물어봤는데, “struct만 선언하면 메모리에 필드가 선언한 순서대로 딱 붙죠?”라고. 나는 잠깐 멈췄다. 교과서에선 그렇게 느껴지지만, 실제론 중간에 빈칸(패딩)이 끼고, 끝에도 꼬리 패딩이 붙는다. 그걸 모른 채 struct를 소켓에 그대로 보내는 코드를 썼다가, “ARM에선 잘 돌아갔는데 x86-64에선 뭔가 4바이트씩 밀리는” 식의 일을 나도 겪었고(그때 offsetof로 필드마다 잡다가 이틀 걸렸다), 학생 눈앞에선 웃고 넘기면서 “일단 printf에 sizeof랑 offsetof 찍어보라”고 말해줬다. 이 글은 그 대화 뒤에 붙는 실전용 흐름이다. 순서는 textbook이 아니다. 먼저 운에 닿는 지점(와이어·ABI·성능)부터 짚고, 그다음에 기초 문법으로 돌아온다.
1. 먼저 터지는 쪽: “구조체만 믿고 바깥(와이어)에 싣기”
운영에서 가장 먼저 터지는 가정이 하나 있어요. “내부에서 struct로 쓰던 레이아웃이, 파일·네트워크·다른 머신에도 그대로 통한다”는 가정이다. 대개 틀린다. 이유는 정렬·패딩뿐 아니라 엔디안, long/size_t/enum 폭, 컴파일 옵션, 심지어 #pragma pack 한 줄이 레이아웃을 갈라버리기도 해서다.
그래서 생산 코드의 상투적 결론은 이거다. 내부 객체는 struct로, 밖으로 나가는 것은 uint8_t 스트림에 명시적으로 조립/파싱한다(엔디안 정책까지 문서에 박는다). 아래처럼 “LE로 간다”를 코드로 못 박는 팀이 많다.
#include <stdint.h>
/* LE로 보낸다 — 바이트 인덱스에 의미 둬야 함 */
static void write_u16_le(unsigned char *p, uint16_t v) {
p[0] = (unsigned char)(v); /* LSB 먼저 */
p[1] = (unsigned char)(v >> 8);
}
static void write_u32_le(unsigned char *p, uint32_t v) {
p[0] = (unsigned char)(v);
p[1] = (unsigned char)(v >> 8);
p[2] = (unsigned char)(v >> 16);
p[3] = (unsigned char)(v >> 24);
}
static uint32_t read_u32_le(const unsigned char *p) {
return (uint32_t)p[0] | ((uint32_t)p[1] << 8)
| ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}
/* enum을 와이어에 "그대로" 쓰지 말고 u8로 고정 */
struct wire_header_le {
uint8_t kind; /* 1=hello, 2=ping … (문서에 표로 따로) */
uint8_t reserved;
uint16_t u16; /* 정책: LE */
uint32_t u32; /* 정책: LE */
};
/* 실전에선 send(이 struct 직투) 말고 버퍼에 pack 한 다음 보낸다 */
enum의 기저 정수 크기는 구현·옵션에 흔들리기 때문에, 원격/파일 쪽 kind는 uint8_t로 폭을 못 박는 편이 안전하다(의미는 #define이나 문서에 상수로 따로). 문법적으로 enum을 “그냥” 쓰는 것과, 와이어에 넣는 정수는 단계를 나누는 게 팀 룰로 정착하는 경우가 많다.
또 하나, struct를 통째로 read()/소켓에 투사하면 엔디안·패딩·정렬 전부가 “내가 생각한 그림”이 아닐 수 있다. 그때 offsetof+_Static_assert로 “지금 이 TU·이 옵션”에서의 상수는 잡을 수 있지만(아래 3절), “상대 머신”까지 같은 이미지를 보장하려면 결국 직렬화가 따로 붙는다.
2. offsetof·sizeof·_Static_assert — “지금 툴체인이 짠 레이아웃”을 잡는 도구
ABI가 박혀 있거나(디스크/네트워크), 파일 헤더 크기를 딱 맞춰야 할 때, 필드에 손이 가면 offsetof로 위치를 다시 봐야 한다. “상수에 숫자 하드코딩”이 아니라, 한 번 측정·고정하는 쪽이 회귀에 강하다.
#include <stdint.h>
#include <stddef.h>
struct on_disk_header {
uint8_t magic[4];
uint16_t version; /* “디스크는 항상 LE” 정책이 있다고 가정 */
uint8_t flags;
uint8_t reserved0;
uint32_t body_len;
};
_Static_assert(sizeof(struct on_disk_header) == 12, "unexpected header size");
_Static_assert(offsetof(struct on_disk_header, body_len) == 8, "unexpected offset");
offsetof는 “이 컴파일/옵션/플랫폼”의 현재 레이아웃이고, 크로스로 “완전 동일”을 원하면, 빌드가 뱉는 상수나 테스트로 회귀시키는 쪽이 현실적이다. 엔디안이 다르면 멀티바이트는 옮긴 뒤 htobe류로 맞추는 식의 또 한 겹이 붙는다(내부 struct와 “와이어 바이트”는 계속 분리).
3. 정렬·패딩, 그리고 “이틀” 걸릴 수 있는 케이스
하드웨어/생성 코드는 타입 T에 대해, 주소가 alignof(T)에 맞을 때 로드/스토어가 빠르거나(혹은 합법)만 쓰인다. 그래서 컴파일러는 struct 안에 중간 패딩을 집어넣고, 배열로 붙일 때를 대비해 꼬리 패딩도 넣는다. sizeof(S)는 “필드 합 + α”이고, α는 정렬·배열 규칙에서 나온다.
디버깅 일화(진짜로 오래 잡힌 유형): __attribute__((packed))로 붙은 헤더를 멀티바이트로 바로 읽다가(정렬 안 맞는 주소) ARM에선 “우연히” 돌아가다가 다른 코어/옵션에서 깨지는. 나는 unaligned uint32_t를 손댄 줄 알고, 이틀 동안 memcpy로 uint32_t 임시에 옮기는 패턴만 뒤졌다. packed는 “작게 보이는” 대신 캐시/unaligned 비용이랑 언어/아키 이슈를 같이 안고온다.
struct naive {
char c1; /* 1 */
int x; /* 앞 3바이트 패딩 — 컴이 알아서 끼움; 너는 그냥 알면 됨 */
char c2; /* 끝 꼬리 패딩 나올 수 있음 */
};
#include <stdio.h>
#include <stddef.h>
int main(void) {
printf("sizeof(naive) = %zu\n", sizeof(struct naive));
printf("offsetof c1 = %zu\n", offsetof(struct naive, c1));
printf("offsetof x = %zu\n", offsetof(struct naive, x));
printf("offsetof c2 = %zu\n", offsetof(struct naive, c2));
return 0;
}
“개념도”는 이렇게 머릿속에 잡는다(실제 sizeof는 측정해라).
flowchart LR
subgraph M["struct naive (개념적 바이트 슬롯)"]
B0["c1"]
P1["pad"]
P2["pad"]
P3["pad"]
I0["x (4 bytes)"]
B1["c2"]
TAIL["pad ..."]
end
필드 순을 int, char, char처럼 큰 것을 앞에 두는 식의 재배치는, ABI/가독성을 해치지 않는 범위에서 공간/캐시에 도움이 될 수 있다(7절).
C11 stdalign로 “더 강한 정렬”이 필요한 경우(캐시 라인, SIMD, MMIO)에 _Alignas를 쓴다.
#include <stdalign.h>
struct hot {
_Alignas(64) double v[8];
int tag;
};
_Static_assert(alignof(struct hot) % 64 == 0, "alignment assumption failed");
packed에 대한 “주의”는 한 문장으로 정리하자. 정렬이 안 맞는 주소에 멀티바이트를 직접 =로 읽는 건(플랫폼/옵션에 따라) 느리거나 위험하다. 팀이 많이 쓰는 해법은 memcpy로 정렬된 임시로 복사해 읽는 것이다.
4. 이제 기초로 내려가기: struct 선언·초기화·. / ->
struct는 사용자 정의 집계 타입이고, 멤버마다 (대부분) 오프셋이 잡힌다. 태그 S는 struct S에서 따로 쓰인다. typedef로 반복 키워드 줄이는 건 팀 스타일이다.
#include <stdio.h>
struct point {
int x;
int y;
};
/* typedef: 팀이 질려서 */
typedef struct {
double re;
double im;
} cpx_t;
int main(void) {
struct point p1;
p1.x = 3;
p1.y = 4;
printf("%d %d\n", p1.x, p1.y);
cpx_t z = {1.0, 0.0};
(void)z;
return 0;
}
struct가 불완전 타입이면(선언만 있고 정의 없음) 그 시점엔 값을 둔 객체는 못 만든다. 전방 선언 struct node; 후 포인터만 쓰는 opaque 핸들의 출발이다(8절).
초기화: 동일한 정적/스레드 지역이면, 안 쓰인 멤버는(규칙에 따라) 0. 자동 지역 struct는 안 건드리면 쓰레기일 수 있어, 실무만 쓰는 기본이 {0}다.
#include <stddef.h>
struct item {
int id;
const char *name;
size_t refcount;
};
void demo_init(void) {
struct item a = {0}; /* 0 훑고 시작 — 패딩까지 0으로 가는 쪽(경고는 옵션) */
struct item b = {1, "foo", 0};
(void)a;
(void)b;
}
memset으로 0을 채우는 건 “비트 0” 관점에선 쓰지만, 부동/포인터/원자 의미까지 항상 ‘논리적 0’을 보장하려 쓰기엔 케이스가 갈린다. 직렬화/역직렬화는 memcpy로 “바이트 이미지”를 다루는 층이 딱 따로인 경우가 많다.
접근: 값이면 s.member, 포인터면 p->member ((*p).member와 같음).
struct buf {
size_t len;
unsigned char *data;
};
void bump_len(struct buf *b) {
if (b == NULL) {
return;
}
b->len += 1U; /* p가 가리키는 쪽 len만 올림 */
}
5. 자주 쓰는 패턴: 복사, designated init, 배열, 중첩, FAM
struct는 대입으로 값 복사다(얕은 복사: 포인터는 주소만).
#include <string.h>
struct chunk {
size_t n;
char *p;
};
void shallow(struct chunk a) {
/* a는 복사본인데, p는 같은 곳 가리킬 수 있음 — 깊은 복사는 따로 */
(void)a;
}
struct chunk dup_chunk(const struct chunk *src) {
struct chunk out = *src;
return out;
}
C99 designated initializer로 필드 이름에 맞춰 넣는다(옵션 늘어날 때 강하다).
struct opts {
int port;
int backlog;
int reuseaddr;
};
struct opts o = {
.port = 8080,
.backlog = 128,
.reuseaddr = 1,
};
struct 배열은 원소마다 sizeof(T) 간격(패딩 포함)이므로, “바이트를 한 줄로” 쓰려면 구조/직렬화는 별도 설계.
struct particle {
float x, y, z;
};
void move(struct particle *ps, size_t n, float dt) {
for (size_t i = 0; i < n; i++) {
ps[i].x += dt; /* 루프 — 다른 struct 크기 가정 끼우지 말 것 */
}
(void)dt;
}
중첩은 그대로 struct 멤버로 품는다.
struct mat3 {
float m[3][3];
};
struct transform {
struct mat3 r;
float tx, ty;
};
int main(void) {
struct transform t = {
.r = {{{1,0,0},{0,1,0},{0,0,1}}},
.tx = 0.0f,
.ty = 0.0f,
};
(void)t;
return 0;
}
FAM(C99, 마지막 멤버만 T arr[])은 malloc(sizeof(struct packet) + n * sizeof(T))로 한 덩어리로 잡는 패턴이 흔하다.
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
struct packet {
uint32_t len;
unsigned char data[]; /* FAM — sizeof에 안 들어감! */
};
struct packet *make_packet(const void *src, size_t n) {
if (n > 0xFFFFFFFFu) {
return NULL;
}
struct packet *p = malloc(sizeof *p + n); /* 꼬리 n바이트까지 같이 */
if (!p) {
return NULL;
}
p->len = (uint32_t)n;
if (n && src) {
memcpy(p->data, src, n);
}
return p;
}
FAM이 있으면 sizeof는 꼬리를 안 셈 → 복제/직렬화는 항상 len 같은 명시 필드로 길이를 쫓는다. 여기서 memcpy 누락이 나오면(페이로드만 잊고) 생각보다 멀쩡해 보이는 버그로 남는다. “한 밤에 잡힌 적 있다” 류는 이런 쪽.
6. union — “같은 메모리, 다른 해석”과 태그
union의 멤버는(대개) 같은 시작을 쓰고, sizeof는 가장 큰 멤버에 맞춰진다. 해석·엔디안은 여전히 조심.
#include <stdio.h>
#include <stdint.h>
union u32b {
uint32_t u;
uint8_t b[4];
};
int main(void) {
union u32b x;
x.u = 0x01020304u;
printf("bytes: %u %u %u %u\n", x.b[0], x.b[1], x.b[2], x.b[3]);
return 0; /* b[] 순서 = 엔디안 따라감; 놀랍지? */
}
활성 멤버와 effective type 규칙 때문에, A로 써놓고 B로 읽는 type punning이 최적화/UB 쪽에 걸릴 수 있다. 이식/안정이 목적이면 memcpy로 unsigned char 루트를 쓰거나, tagged union으로 “지금 유효한 멤”을 한 갈래로 못 박는다(엄격 별칭·#05 포인터 연결).
#include <string.h>
#include <stdint.h>
float u32_to_float_pun(uint32_t bits) {
uint32_t tmp = bits;
float f;
memcpy(&f, &tmp, sizeof f); /* float 비트를 그냥 떠올릴 땐 이쪽이 무난 */
return f;
}
enum value_kind { VAL_INT, VAL_FLOAT, VAL_STR };
struct value {
enum value_kind k; /* 이거 없이 u만 건드리면… 그날의 나한테 애도 */
union {
int i;
float f;
const char *s;
} u;
};
void use_value(const struct value *v) {
if (!v) {
return;
}
switch (v->k) {
case VAL_INT: (void)v->u.i; break;
case VAL_FLOAT: (void)v->u.f; break;
case VAL_STR: (void)v->u.s; break;
}
}
메시지를 한 struct msg에 모을 때도 type을 먼저 보고 union을 읽는 파서 습관이 생명선이다(분기 전에 u 건드리지 않기). 프로덕션에선 I/O 버퍼 크기를 union의 최악 크기+패딩까지 생각해 잡는다.
#include <stdint.h>
#include <stddef.h>
struct msg_hello { uint8_t v; uint16_t cap; };
struct msg_ping { uint32_t ts; };
struct msg_data { uint32_t n; };
enum msg_type { MSG_HELLO, MSG_PING, MSG_DATA };
struct msg {
uint8_t type;
union {
struct msg_hello hello;
struct msg_ping ping;
struct msg_data data;
} u;
};
_Static_assert(sizeof(struct msg) >= 1, "sanity");
7. 비트필드 — 드라이버에서 빛나고, 이식에선…
struct flags {
unsigned a : 3;
unsigned b : 1;
unsigned : 0; /* 다음 “단위” 끊는 효과 — 구현 의존이 많음 */
unsigned c : 4;
};
부호·비트 수·“한 unsigned에 몇 필드가 붙는지”는 정의가 구현에 흔들리고, 주소 &도 기대하기 어렵다. 엔디안+컴파일러에 묶여 딴 머신에서 “레지스터 비트맵 = 소스 1:1” 재현이 어렵다.
임베디드/드라이버에서 32비트 레지스터를 쓰는 전형은 이렇게—비트필드 대신 shift/mask로 못 박는 팀이 많다.
#include <stdint.h>
#if defined(__GNUC__)
typedef struct {
volatile uint32_t v;
} reg32_t;
#define IO_GPIO_MODE (*(reg32_t *)0x40021000U)
#define GPIO_MODE_OUT_SHIFT 0u
#define GPIO_MODE_MASK 0x3u
/* 여기 uint32_t에 shift/mask — 비트필드 1:1 믿지 말고 */
#endif
이식이 먼저면 uint32_t+((x>>s)&m), 한 SoC+한 툴체인에 고정된 레지스터면(그리고 CI에 크로스 빌드 검증) 비트필드가 팀 룰일 수는 있다. “네트워크/원격/파일”이랑 섞이지 않게—도메인 밖으로 나가면 깨질 확률이 큼.
8. ABI·packed·OS API
struct stat 류 OS API는 플랫폼에 오프셋이 고정된 경우가 있고(헤더가 근거), 앱이 struct를 “디스크/네트워크 그대로” 쓰면 엔디안·패딩·정렬·버전이 조금만 달라도 호환이 끊긴다. 그래서 바깥은 uint8_t/uint32_t/uint64_t+엔디안+스키마 버전+필요 시 Protobuf/TLV 같은 명시가 일반론.
GCC/Clang __attribute__((packed)) 예(확장):
#include <stdint.h>
struct __attribute__((packed)) net_hdr {
uint8_t ver;
uint16_t u16; /* unaligned read 경고/비효율 — 플랫폼 따라 다름; memcpy 쪽 선호 */
uint32_t u32;
};
bool·long·size_t·enum·포인터 폭을 와이어에서 기대하는 건 위험하고, #pragma pack/비트필드/FAM/_Alignas는 옵션 민감하다. 결론은 1절과 같다: 내부는 struct, 와이어는 serialize/deserialize.
9. 성능: 필드 순서, 캐시 라인, false sharing
큰 먼저, 작은/비슷한 것끼리 묶으면 꼬리 패딩을 줄여 객체·배열 전체의 메모리를 아낄 수 있다. 자주 같이 쓰는 필드를 같은/인접 캐시 라인에 두면(측정 전제) 미스·충돌을 줄이는 기대는 할 수 있다.
false sharing은 이런 켸스다. 코어0이 필드A, 코어1이 필드B를 “서로 독립”인 줄 알았는데 같은 64B 라인에 앉아 있으면, 잠금 없이도 캐시 무효화가 서로를 때린다. 며칠 잡힌 적이 있다고 말해도 과장이 아닌, perf로 라인 쪼갠 뒤 alignas(64)로 핫 카운터 띄우는 쪽(정확한 수치는 프로파일).
flowchart TB
subgraph C["캐시 라인(예: 64B)"]
A["필드A (코어0가 자주)"]
B["필드B (코어1가 자주)"]
end
C2["같은 라인 → 둘 다 invalidation (false sharing)"]
A --> C2
B --> C2
10. Opaque pointer + 파일 포맷 예
Opaque: 헤더엔 struct engine;만, .c에 정의.
/* api.h */
struct engine;
struct engine *engine_new(void);
void engine_free(struct engine *e);
void engine_tick(struct engine *e, double dt);
/* engine.c */
#include "api.h"
#include <stdlib.h>
struct engine {
int state;
/* 밖에선 sizeof도 모름 — offsetof 의존 못 함 */
};
struct engine *engine_new(void) { return calloc(1, sizeof(struct engine)); }
void engine_free(struct engine *e) { free(e); }
void engine_tick(struct engine *e, double dt) { (void)dt; if (e) e->state++; }
파일 v1 헤더는 크기·오프셋을 빌드에서 끊는다.
#include <stdint.h>
#include <stddef.h>
struct file_header_v1 {
uint8_t magic[4];
uint16_t version;
uint16_t flags;
uint32_t table_off;
uint32_t table_len;
};
_Static_assert(sizeof(struct file_header_v1) == 16, "v1 size fixed");
/* 엔디안은 read 후 정책에 맞게 */
11. 망할 때 뭐부터 보나 — “표” 대신 머릿속 체크리스트
sizeof가 유독 크다 싶으면, 정·꼬리 패딩, double/SIMD 쪽 큰 정렬, 멤버 순 재배치(ABI가 허용하는 범위)부터 의심한다. 다른 머신에선 엔디안, 정수 폭, 옵션, packed가 레이아웃을 흔든다—와이어는 바이트+명시로 고정.
union이 수상(최적화로 “안 읽힌다”)하면 활성 멤버/별칭 쪽. 태그+분기, memcpy 경로, UBSan·옵션을 같이 본다. unaligned uint32_t는 packed+직접 역참조 쪽—memcpy로 정렬된 곳에 옮겨 읽는다. 비트필드는 문서와 안 맞으면 SoC+툴체인+엔디안+부호+경계를 의심하고, 그냥 시프트 API로 갈아타는 팀이 많다.
캐시/스레딩이 느리면 false sharing(7절)과 핫/콜드 섞임. FAM이면 sizeof에 페이로드 없음—len만 믿고 malloc+memcpy. memset(0) 뒤 부동/포인터를 “0처럼” 기대하는 건 타입·플랫폼·표준 해석이 엮인다.
또 한 번: 원격/파일 enum 직투는, 크기·부호가 흔들려 깨질 수 있다. uint8_t 등으로 폭을 박는다.
#include <stdint.h>
#define MSG_HELLO 1u
#define MSG_PING 2u
struct wire {
uint8_t kind; /* 1,2,… (raw enum 아님) */
uint8_t rsv0;
uint16_t u16;
};
12. C struct vs C++ class/struct — 핵심만 흐름으로
C와 C++는 비슷한 문법이 있어도, ODR, 접근 제어, 템플릿, 수명, RAII, 맹글링이 다르다. C++에서 C 헤더 끼울 땐 extern "C" 루틴이 병으로 붙는 경우가 있다.
C의 struct엔 “멤버 private 기본” 같은 게 없다. C++ class는 기본 private, struct는 public—여기서 오해 나오기 쉽다. C++은 멤버 함수·생성자, 템플릿, 오버로드로 타입이 커지고, 이진으로 “C struct랑 똑같이” 맞출 생각이면(상속/가상까지) 꿈이 깨진다. 이진/원격은 공용 바이트 스키마+코덱 쪽이 안전.
요약
struct의 소스 상 필드와 런타임 이미지는 항상 같지 않다. offsetof+_Static_assert로 “이 툴체인·옵션”을 검증하되, 와이어/파일은 엔디안·스키마·버전이 별 축이다. union은 강력하지만 활성 멤/타입 해석/별칭이 걸리고, 비트필드는 단일 SoC 밖이면 시프트/마스크로 가는 팀이 많다. 성능은 packed뿐 아니라 캐시 라인(가짜 공유)이 한 번에 갈아엎는다.
다음: #08 전처리 단계
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 임베디드/드라이버(레지스터·DMA), OS API(struct ABI), 파서/프로토콜(태그드 유니온), 파일 포맷(고정 헤더), 저지연(캐시/정렬)까지 이어집니다. “그냥 struct를 소켓에”는 예외(동일 머신·같은 컴파일·문서가 확실)에서만, 그 외엔 직렬화/역직렬화를 씁니다.
Q. union의 한 멤버로 memcpy로 덮고 다른 멤버를 읽어도 되나요?
A. C의 규정은 “컨텍스트/구현/최적화”에 민감합니다. 이식/안정이 목표면, 태그+분기 또는 unsigned char/memcpy 기반 바이트 이미지로 의미를 한 방향으로 고정하세요(필요 시 volatile/메모리 모델은 별도).
Q. packed는 언제 써도 되나요?
A. “꼭 필요한(레거시/하드웨어/팀 룰로 고정된) 케이스”에서, 그리고 성능/정렬 UB 위험을 인지한 채로, 보통 memcpy 경로/테스트/정적 어서트와 같이 씁니다. 이식이 먼저면 와이어는 uint8_t 스트림이 더 흔합니다.
Q. 선행으로 읽으면 좋은 글은?
A. C 언어 시리즈 #02 — 타입/정수/패딩, #04 — 함수/ABI/가변인자, #05 — 포인터/엄격 별칭을 권장합니다. C 언어 시리즈 목차에서 전체 흐름을 확인하세요.
Q. 더 깊이 공부하려면?
A. C 표준(특히 구조/공용/비트필드/유효 타입)과, 실무적으로는 cppreference의 C documentation과, 플랫폼의 ABI 문서(예: x86-64 System V), 컴파일러 매뉴얼(확장 속성)을 병행하는 것이 좋습니다.
같이 보면 좋은 글 (내부 링크)
- C 언어 시리즈 #04 — 함수·스택 프레임·호출 규약(ABI)과 가변 인자
- C 언어 시리즈 #05 — 포인터 연산·엄격 별칭(strict aliasing)·유효성
- C 언어 시리즈 #02 — 타입·승격(usual arithmetic)·정수 표현과 패딩
이 글에서 다루는 키워드 (관련 검색어)
C, struct, union, 비트필드, ABI, offsetof, padding, alignment, FAM, tagged union, type punning, endian 등으로 검색하시면 이 글을 찾는 데 도움이 됩니다.