본문으로 건너뛰기
Previous
Next
C 언어 시리즈 #07 — 구조체·공용체·비트필드·패딩과 ABI

C 언어 시리즈 #07 — 구조체·공용체·비트필드·패딩과 ABI

C 언어 시리즈 #07 — 구조체·공용체·비트필드·패딩과 ABI

이 글의 핵심

왜 컴파일러가 패딩을 넣는지, offsetof가 왜 필요한지, union으로 타입을 바꿔 읽기가 왜 엄격 별칭과 충돌하는지, 비트필드가 endian·정렬에 어떻게 묶이는지 정리합니다.

시리즈 안내

#07 | 📋 전체 목차 | 이전: #06 배열 · 다음: #08 전처리기

지난주에 학생이 물어봤는데, “struct만 선언하면 메모리에 필드가 선언한 순서대로 딱 붙죠?”라고. 나는 잠깐 멈췄다. 교과서에선 그렇게 느껴지지만, 실제론 중간에 빈칸(패딩)이 끼고, 끝에도 꼬리 패딩이 붙는다. 그걸 모른 채 struct소켓에 그대로 보내는 코드를 썼다가, “ARM에선 잘 돌아갔는데 x86-64에선 뭔가 4바이트씩 밀리는” 식의 일을 나도 겪었고(그때 offsetof로 필드마다 잡다가 이틀 걸렸다), 학생 눈앞에선 웃고 넘기면서 “일단 printfsizeofoffsetof 찍어보라”고 말해줬다. 이 글은 그 대화 뒤에 붙는 실전용 흐름이다. 순서는 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기저 정수 크기는 구현·옵션에 흔들리기 때문에, 원격/파일 쪽 kinduint8_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를 손댄 줄 알고, 이틀 동안 memcpyuint32_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는 사용자 정의 집계 타입이고, 멤버마다 (대부분) 오프셋이 잡힌다. 태그 Sstruct 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 쪽에 걸릴 수 있다. 이식/안정이 목적이면 memcpyunsigned 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 statOS 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_tpacked+직접 역참조 쪽—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, struct, union, 비트필드, ABI, offsetof, padding, alignment, FAM, tagged union, type punning, endian 등으로 검색하시면 이 글을 찾는 데 도움이 됩니다.