C 구조체 마스터: 정의, 멤버 접근, 중첩, 정렬, 패딩, 비트필드
구조체(struct)는 서로 다른 타입의 변수들을 하나로 묶어 사용자 정의 타입을 만드는 핵심 기능입니다. 레코드 표현, 자료구조, 하드웨어 매핑 등에 필수적입니다.
구조체 정의와 선언
// 태그 있는 구조체
struct Point {
int x;
int y;
};
// typedef로 별칭 생성 (권장)
typedef struct {
char name[50];
int age;
double salary;
} Employee;
// 태그 + typedef 동시
typedef struct Node {
int data;
struct Node *next; // 자기 참조 포인터 (태그 필요)
} Node;
변수 선언과 초기화
struct Point p1; // 선언
struct Point p2 = {10, 20}; // 순서대로 초기화
struct Point p3 = {.x = 10, .y = 20}; // 지정 초기화자 (C99, 순서 무관)
Employee e = {"Alice", 30, 50000.0};
Node *head = NULL;
멤버 접근
| 연산자 | 피연산자 | 예시 |
. | 구조체 변수 | p1.x = 5; |
-> | 구조체 포인터 | p->x = 5; (≡ (*p).x) |
구조체 복사/대입/비교
대입(=): 멤버 단위 얕은 복사 (값 타입 멤버는 복사, 포인터 멤버는 주소 복사)
비교(==): 불가 (연산자 없음) → 멤버별 비교 함수 직접 작성
전달: 값 전달 시 전체 복사 (큰 구조체는 포인터 전달 권장)
struct Point a = {1,2}, b;
b = a; // OK: 멤버별 복사
// 비교 함수
int point_eq(struct Point a, struct Point b) {
return a.x == b.x && a.y == b.y;
}
중첩 구조체
struct Date { int y, m, d; };
struct Person {
char name[32];
struct Date birth; // 중첩 구조체 (값 포함)
struct Date *hired; // 포인터로 참조
};
struct Person p = {
.name = "Bob",
.birth = {1990, 5, 15}, // 중첩 초기화
.hired = &(struct Date){2020, 1, 1} // 복합 리터럴 (C99)
};
printf("%d-%02d-%02d\n", p.birth.y, p.birth.m, p.birth.d);
메모리 레이아웃: 정렬(Alignment)과 패딩(Padding)
struct Example {
char c; // 1바이트, 오프셋 0
// 패딩 3바이트 (다음 int 정렬 위해)
int i; // 4바이트, 오프셋 4
double d; // 8바이트, 오프셋 8 (8의 배수)
// 패딩 없음 (이미 16의 배수)
};
// 크기: 24바이트 (1 + 3패딩 + 4 + 8 + 8패딩? → 실제 24)
// 멤버 순서 재배치로 크기 절약
struct Optimized {
double d; // 오프셋 0
int i; // 오프셋 8
char c; // 오프셋 12
// 패딩 3바이트 → 총 16바이트
};
패딩 확인 매크로
#define OFFSET_OF(type, member) ((size_t)&(((type*)0)->member))
#define SIZE_OF(type) sizeof(type)
printf("c offset: %zu\n", OFFSET_OF(struct Example, c)); // 0
printf("i offset: %zu\n", OFFSET_OF(struct Example, i)); // 4
printf("d offset: %zu\n", OFFSET_OF(struct Example, d)); // 8
printf("size: %zu\n", SIZE_OF(struct Example)); // 24
비트필드 (Bit-fields)
구조체 내에서 비트 단위 멤버 정의로 메모리 절약.
struct Flags {
unsigned int ready : 1;
unsigned int error : 1;
unsigned int mode : 2; // 0~3
unsigned int code : 4; // 0~15
// 총 8비트 = 1바이트 (패딩 포함 보통 4바이트)
};
제약사항
타입: int, signed int, unsigned int, _Bool (C99)
폭 0: 이름 없는 패딩 비트 (정렬용)
배열/포인터/함수 불가
& 연산 불가 (주소 없음)
구현 의존적: 비트 순서, 패킹 방식, 타입 크기
유연한 배열 멤버 (Flexible Array Member, C99)
구조체 끝에 크기 미정 배열로 가변 길이 데이터 저장.
struct String {
size_t len;
char data[]; // 유연한 배열 멤버 (마지막 멤버만 가능)
};
// 할당
struct String *s = malloc(sizeof(struct String) + 100);
s->len = 5;
memcpy(s->data, "Hello", 6); // 널 포함 복사
구조체와 함수
값 전달 vs 포인터 전달
// 값 전달 (복사본 수정)
void move_val(struct Point p, int dx, int dy) {
p.x += dx; p.y += dy;
}
// 포인터 전달 (원본 수정)
void move_ptr(struct Point *p, int dx, int dy) {
p->x += dx; p->y += dy;
}
// const 포인터로 읽기 전용
void print_point(const struct Point *p) {
printf("(%d, %d)\n", p->x, p->y);
}
구조체 반환
struct Point midpoint(struct Point a, struct Point b) {
return (struct Point){ (a.x+b.x)/2, (a.y+b.y)/2 }; // 복합 리터럴 반환
}
불완전 타입 (Incomplete Type) / 전방 선언
// 헤더 파일 (불완전 타입으로 포인터만 노출)
struct Opaque; // 전방 선언
typedef struct Opaque Opaque;
Opaque *create();
void destroy(Opaque *);
void do_something(Opaque *);
// .c 파일에서 정의
struct Opaque {
int hidden_data;
double secret;
};
→ 정보 은닉, 컴파일 의존성 감소, ABI 안정성
모범 사례
typedef struct { ... } Name; 패턴으로 태그 생략 가능
멤버 정렬 고려해 큰 타입부터 선언 (패딩 최소화)
큰 구조체는 포인터로 전달, const 정확성 유지
비트필드는 하드웨어 레지스터/프로토콜 매핑 외엔 신중히 사용
불완전 타입으로 캡슐화 구현 (Opaque Pointer 패턴)
offsetof 매크로(<stddef.h>)로 오프셋 계산