Ver3.5 C언어 구조체와 공용체를 활용한 효율적 데이터 구조 설계 전략

C언어 구조체와 공용체를 활용한 효율적 데이터 구조 설계 전략
struct & union, 제대로 알고 쓰면 진짜 레전드 코드 나옴 🔥
야 솔직히 말해서, C언어 배울 때 포인터 다음으로 사람들이 제일 헷갈려하는 게 바로 구조체(struct)랑 공용체(union)임 ㅋㅋㅋ
"아 그냥 변수 여러 개 묶는 거 아니야?" 라고 생각하는 순간... 그건 진짜 빙산의 일각만 본 거임.
이 두 가지를 제대로 이해하고 설계에 활용하면, 메모리 효율이 극적으로 올라가고 코드 가독성도 미쳐버림 🚀
오늘은 그냥 문법 설명 말고, 실제로 어떻게 데이터 구조를 설계하는지 팩트 위주로 빡세게 파봄.
구조체는 서로 다른 타입의 변수들을 하나의 논리적 단위로 묶는 사용자 정의 타입임.
C언어에서 객체지향 흉내를 낼 수 있는 가장 기본적인 도구이기도 하고, 실무에서도 진짜 많이 씀.
"그냥 변수 여러 개 선언하면 되지 않냐?" 라고 물어보면... 그건 관리가 안 됨 ㅋㅋ 100개 학생 데이터 관리할 때 변수 300개 선언할 거임? 😅
// 구조체 정의
struct Student {
int id; // 학번
char name[50]; // 이름
float gpa; // 학점
int age; // 나이
};
// 변수 선언 방법 1: 구조체 정의 후 별도 선언
struct Student s1;
// 변수 선언 방법 2: typedef로 타입 별칭 지정 (실무에서 이게 더 많이 쓰임)
typedef struct {
int id;
char name[50];
float gpa;
int age;
} Student;
Student s1; // struct 키워드 없이 바로 사용 가능!
// 방법 1: 순서대로 초기화 (C89/C90 스타일)
Student s1 = {20231001, "김철수", 3.8f, 21};
// 방법 2: 지정 초기화 (C99 이후, 이게 더 안전함!)
Student s2 = {
.id = 20231002,
.name = "이영희",
.gpa = 4.0f,
.age = 22
};
// 방법 3: 멤버 접근 후 개별 초기화
Student s3;
s3.id = 20231003;
strcpy(s3.name, "박민준"); // 문자열은 strcpy 써야 함!
s3.gpa = 3.5f;
s3.age = 20;
방법 2의 지정 초기화(Designated Initializer)가 진짜 꿀임 ㅋㅋ
멤버 순서 바꿔도 안전하고, 어떤 멤버에 뭘 넣는지 코드만 봐도 바로 알 수 있어서 유지보수할 때 개편함 👍
여기서부터 진짜 중요한 내용 나옴. 많은 사람들이 모르는 부분인데, 구조체는 메모리 정렬(Alignment) 규칙 때문에 예상보다 더 많은 메모리를 차지할 수 있음.
#include <stdio.h>
typedef struct {
char a; // 1 byte
int b; // 4 bytes
char c; // 1 byte
} BadLayout;
typedef struct {
int b; // 4 bytes
char a; // 1 byte
char c; // 1 byte
} GoodLayout;
int main() {
printf("BadLayout 크기: %zu bytes\n", sizeof(BadLayout)); // 12 bytes!
printf("GoodLayout 크기: %zu bytes\n", sizeof(GoodLayout)); // 8 bytes
return 0;
}
이게 바로 구조체 멤버 배치 최적화의 핵심임. 규칙은 간단함:
큰 타입 → 작은 타입 순서로 배치하면 패딩이 최소화됨.
| 타입 | 크기 | 정렬 요구사항 |
|---|---|---|
| char | 1 byte | 1 byte 경계 |
| short | 2 bytes | 2 byte 경계 |
| int | 4 bytes | 4 byte 경계 |
| long long | 8 bytes | 8 byte 경계 |
| float | 4 bytes | 4 byte 경계 |
| double | 8 bytes | 8 byte 경계 |
// 패딩 없이 꽉꽉 채워서 저장 (네트워크 프로토콜, 파일 포맷 등에서 사용)
#pragma pack(1)
typedef struct {
char type; // 1 byte
int length; // 4 bytes
short version; // 2 bytes
} PacketHeader; // 정확히 7 bytes
#pragma pack() // 원래 정렬 방식으로 복구
// 또는 GCC에서는 __attribute__((packed)) 사용
typedef struct {
char type;
int length;
short version;
} __attribute__((packed)) PacketHeader2;
구조체를 진짜 잘 쓰려면 포인터랑 같이 써야 함. 함수에 구조체를 넘길 때 값 복사로 넘기면 메모리 낭비가 심하거든. 특히 구조체가 크면 클수록 더 심각함 😱
typedef struct {
int id;
char name[50];
float gpa;
} Student;
void printStudent(Student *s) { // 포인터로 받기 (값 복사 없음!)
// 포인터로 멤버 접근할 때는 -> 연산자 사용
printf("ID: %d, 이름: %s, 학점: %.2f\n", s->id, s->name, s->gpa);
// (*s).id 와 s->id 는 완전히 동일한 표현
// -> 가 훨씬 깔끔하니까 이걸 씀
}
void updateGpa(Student *s, float newGpa) {
s->gpa = newGpa; // 원본 데이터 직접 수정 가능
}
int main() {
Student s1 = {.id=1001, .name="김철수", .gpa=3.5f};
printStudent(&s1); // 주소 전달
updateGpa(&s1, 4.0f);
printStudent(&s1); // 수정된 값 확인
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
int id;
char *name; // 동적 문자열 (메모리 절약!)
float gpa;
} Student;
// 학생 생성 함수 (생성자 패턴)
Student* createStudent(int id, const char *name, float gpa) {
Student *s = (Student*)malloc(sizeof(Student));
if (s == NULL) return NULL;
s->id = id;
s->name = (char*)malloc(strlen(name) + 1);
if (s->name == NULL) {
free(s);
return NULL;
}
strcpy(s->name, name);
s->gpa = gpa;
return s;
}
// 학생 해제 함수 (소멸자 패턴)
void destroyStudent(Student *s) {
if (s != NULL) {
free(s->name); // 내부 동적 메모리 먼저 해제
free(s); // 구조체 자체 해제
}
}
int main() {
// 동적 배열로 학생 10명 관리
int n = 10;
Student **students = (Student**)malloc(n * sizeof(Student*));
students[0] = createStudent(1001, "김철수", 3.8f);
students[1] = createStudent(1002, "이영희", 4.0f);
// ... 나머지 초기화
// 사용 후 메모리 해제
for (int i = 0; i < 2; i++) {
destroyStudent(students[i]);
}
free(students);
return 0;
}
typedef struct {
int year;
int month;
int day;
} Date;
typedef struct {
float x;
float y;
} Point;
typedef struct {
int id;
char name[50];
Date birthDate; // 구조체 안에 구조체!
Point location; // 또 다른 중첩 구조체
} Employee;
int main() {
Employee emp = {
.id = 2001,
.name = "박민준",
.birthDate = {.year=1995, .month=3, .day=15},
.location = {.x=37.5665f, .y=126.9780f}
};
// 중첩 멤버 접근
printf("생년월일: %d년 %d월 %d일\n",
emp.birthDate.year,
emp.birthDate.month,
emp.birthDate.day);
// 포인터로 접근할 때
Employee *pEmp = &emp;
printf("위치: (%.4f, %.4f)\n",
pEmp->location.x,
pEmp->location.y);
return 0;
}
공용체는 구조체랑 문법이 비슷한데, 동작 방식이 완전히 다름.
모든 멤버가 같은 메모리 공간을 공유하고, 크기는 가장 큰 멤버 기준으로 결정됨.
"그럼 동시에 여러 멤버 값 저장 못하는 거 아니야?" 맞음! 그게 바로 공용체의 특성이고, 이걸 잘 활용하는 게 포인트임 ㅋㅋ
#include <stdio.h>
typedef union {
int i;
float f;
char c;
} Data;
int main() {
Data d;
d.i = 42;
printf("int: %d\n", d.i); // 42
printf("float: %f\n", d.f); // 쓰레기값! (int 비트를 float으로 해석)
printf("char: %c\n", d.c); // 쓰레기값!
d.f = 3.14f;
printf("float: %f\n", d.f); // 3.140000
printf("int: %d\n", d.i); // 쓰레기값! (float 비트를 int로 해석)
// 핵심: 마지막에 저장한 멤버만 유효한 값을 가짐!
printf("union 크기: %zu bytes\n", sizeof(Data)); // 4 bytes (가장 큰 멤버 기준)
return 0;
}
공용체의 가장 유명한 활용법 중 하나가 바로 타입 펀닝(Type Punning)임.
비트 패턴을 그대로 유지하면서 다른 타입으로 해석하는 기법인데, 임베디드나 시스템 프로그래밍에서 진짜 많이 씀.
// float의 비트 패턴을 int로 보기 (IEEE 754 분석)
typedef union {
float f;
unsigned int bits;
struct {
unsigned int mantissa : 23; // 가수부
unsigned int exponent : 8; // 지수부
unsigned int sign : 1; // 부호
} parts;
} FloatBits;
int main() {
FloatBits fb;
fb.f = -3.14f;
printf("float 값: %f\n", fb.f);
printf("비트 패턴: 0x%08X\n", fb.bits);
printf("부호: %u\n", fb.parts.sign); // 1 (음수)
printf("지수부: %u\n", fb.parts.exponent); // 128
printf("가수부: %u\n", fb.parts.mantissa); // 4788187
return 0;
}
이게 진짜 공용체의 킬러 앱임 ㅋㅋ 여러 타입의 데이터를 하나의 구조로 처리할 때 씀.
// 다양한 타입의 값을 하나의 구조체로 처리
typedef enum {
TYPE_INT,
TYPE_FLOAT,
TYPE_STRING
} ValueType;
typedef struct {
ValueType type; // 현재 어떤 타입인지 태그
union {
int iVal;
float fVal;
char sVal[64];
} data; // 실제 데이터 (공용체)
} Variant;
// 값 출력 함수
void printVariant(const Variant *v) {
switch (v->type) {
case TYPE_INT:
printf("int: %d\n", v->data.iVal);
break;
case TYPE_FLOAT:
printf("float: %.2f\n", v->data.fVal);
break;
case TYPE_STRING:
printf("string: %s\n", v->data.sVal);
break;
}
}
int main() {
Variant values[3];
values[0].type = TYPE_INT;
values[0].data.iVal = 42;
values[1].type = TYPE_FLOAT;
values[1].data.fVal = 3.14f;
values[2].type = TYPE_STRING;
strcpy(values[2].data.sVal, "안녕하세요!");
for (int i = 0; i < 3; i++) {
printVariant(&values[i]);
}
return 0;
}
이 패턴을 태그된 공용체(Tagged Union) 또는 판별 공용체(Discriminated Union)라고 부름.
C++의 std::variant, Rust의 enum이 이 개념을 언어 차원에서 지원하는 거임.
C에서는 이렇게 직접 구현해야 하는데, 이게 JSON 파서, 스크립팅 언어 인터프리터, 데이터베이스 엔진 등에서 핵심 패턴으로 쓰임 🔥
// 네트워크 패킷 헤더 파싱 예시
#pragma pack(1)
typedef struct {
uint8_t version;
uint8_t type;
uint16_t length;
uint32_t sequence;
} CommonHeader;
typedef struct {
CommonHeader header;
uint32_t src_ip;
uint32_t dst_ip;
uint8_t data[256];
} DataPacket;
typedef struct {
CommonHeader header;
uint8_t error_code;
char message[128];
} ErrorPacket;
typedef union {
CommonHeader header; // 공통 헤더로 타입 먼저 확인
DataPacket data; // 데이터 패킷
ErrorPacket error; // 에러 패킷
uint8_t raw[512]; // 원시 바이트 접근
} Packet;
#pragma pack()
void processPacket(Packet *pkt) {
// 헤더의 type 필드로 패킷 종류 판별
switch (pkt->header.type) {
case 0x01:
printf("데이터 패킷: src=%u, dst=%u\n",
pkt->data.src_ip, pkt->data.dst_ip);
break;
case 0xFF:
printf("에러 패킷: code=%d, msg=%s\n",
pkt->error.error_code, pkt->error.message);
break;
}
}
이제 진짜 실전임 ㅋㅋ 구조체를 활용해서 실제 자료구조를 구현해보자.
이걸 이해하면 C언어로 뭐든 만들 수 있다는 자신감이 생김 💪
구조체의 자기 참조(Self-referential)를 이용한 가장 기본적인 동적 자료구조임.
#include <stdio.h>
#include <stdlib.h>
// 자기 참조 구조체 (Self-referential struct)
typedef struct Node {
int data;
struct Node *next; // 같은 타입의 포인터! (typedef 완성 전이라 struct Node 써야 함)
} Node;
typedef struct {
Node *head;
int size;
} LinkedList;
// 리스트 초기화
LinkedList* createList() {
LinkedList *list = (LinkedList*)malloc(sizeof(LinkedList));
list->head = NULL;
list->size = 0;
return list;
}
// 앞에 노드 추가 O(1)
void pushFront(LinkedList *list, int data) {
Node *newNode = (Node*)malloc(sizeof(Node));
newNode->data = data;
newNode->next = list->head;
list->head = newNode;
list->size++;
}
// 뒤에 노드 추가 O(n)
void pushBack(LinkedList *list, int data) {
Node *newNode = (Node*)malloc(sizeof(Node));
newNode->data = data;
newNode->next = NULL;
if (list->head == NULL) {
list->head = newNode;
} else {
Node *curr = list->head;
while (curr->next != NULL) curr = curr->next;
curr->next = newNode;
}
list->size++;
}
// 리스트 출력
void printList(LinkedList *list) {
Node *curr = list->head;
printf("[");
while (curr != NULL) {
printf("%d", curr->data);
if (curr->next != NULL) printf(" → ");
curr = curr->next;
}
printf("]\n");
}
// 메모리 해제
void destroyList(LinkedList *list) {
Node *curr = list->head;
while (curr != NULL) {
Node *next = curr->next;
free(curr);
curr = next;
}
free(list);
}
int main() {
LinkedList *list = createList();
pushBack(list, 10);
pushBack(list, 20);
pushBack(list, 30);
pushFront(list, 5);
printList(list); // [5 → 10 → 20 → 30]
printf("크기: %d\n", list->size);
destroyList(list);
return 0;
}
#define MAX_SIZE 100
typedef struct {
int data[MAX_SIZE];
int top;
} Stack;
void initStack(Stack *s) { s->top = -1; }
int isEmpty(Stack *s) { return s->top == -1; }
int isFull(Stack *s) { return s->top == MAX_SIZE - 1; }
int push(Stack *s, int val) {
if (isFull(s)) return 0; // 실패
s->data[++s->top] = val;
return 1; // 성공
}
int pop(Stack *s, int *val) {
if (isEmpty(s)) return 0; // 실패
*val = s->data[s->top--];
return 1; // 성공
}
int peek(Stack *s, int *val) {
if (isEmpty(s)) return 0;
*val = s->data[s->top];
return 1;
}
int main() {
Stack s;
initStack(&s);
push(&s, 10);
push(&s, 20);
push(&s, 30);
int val;
while (!isEmpty(&s)) {
pop(&s, &val);
printf("%d ", val); // 30 20 10 (LIFO!)
}
printf("\n");
return 0;
}
typedef struct TreeNode {
int data;
struct TreeNode *left;
struct TreeNode *right;
} TreeNode;
TreeNode* createNode(int data) {
TreeNode *node = (TreeNode*)malloc(sizeof(TreeNode));
node->data = data;
node->left = node->right = NULL;
return node;
}
// 이진 탐색 트리 삽입
TreeNode* insert(TreeNode *root, int data) {
if (root == NULL) return createNode(data);
if (data < root->data)
root->left = insert(root->left, data);
else if (data > root->data)
root->right = insert(root->right, data);
return root;
}
// 중위 순회 (정렬된 순서로 출력)
void inorder(TreeNode *root) {
if (root == NULL) return;
inorder(root->left);
printf("%d ", root->data);
inorder(root->right);
}
// 트리 메모리 해제
void destroyTree(TreeNode *root) {
if (root == NULL) return;
destroyTree(root->left);
destroyTree(root->right);
free(root);
}
int main() {
TreeNode *root = NULL;
int values[] = {5, 3, 7, 1, 4, 6, 8};
for (int i = 0; i < 7; i++)
root = insert(root, values[i]);
printf("중위 순회: ");
inorder(root); // 1 3 4 5 6 7 8 (정렬됨!)
printf("\n");
destroyTree(root);
return 0;
}
#define TABLE_SIZE 101
typedef struct HashNode {
char key[64];
int value;
struct HashNode *next; // 체이닝으로 충돌 해결
} HashNode;
typedef struct {
HashNode *buckets[TABLE_SIZE];
int count;
} HashTable;
// 해시 함수 (djb2 알고리즘)
unsigned int hash(const char *key) {
unsigned int h = 5381;
int c;
while ((c = *key++))
h = ((h << 5) + h) + c; // h * 33 + c
return h % TABLE_SIZE;
}
HashTable* createHashTable() {
HashTable *ht = (HashTable*)calloc(1, sizeof(HashTable));
return ht;
}
void htSet(HashTable *ht, const char *key, int value) {
unsigned int idx = hash(key);
HashNode *curr = ht->buckets[idx];
// 기존 키 업데이트
while (curr != NULL) {
if (strcmp(curr->key, key) == 0) {
curr->value = value;
return;
}
curr = curr->next;
}
// 새 노드 추가
HashNode *newNode = (HashNode*)malloc(sizeof(HashNode));
strncpy(newNode->key, key, 63);
newNode->value = value;
newNode->next = ht->buckets[idx];
ht->buckets[idx] = newNode;
ht->count++;
}
int htGet(HashTable *ht, const char *key, int *value) {
unsigned int idx = hash(key);
HashNode *curr = ht->buckets[idx];
while (curr != NULL) {
if (strcmp(curr->key, key) == 0) {
*value = curr->value;
return 1; // 찾음
}
curr = curr->next;
}
return 0; // 못 찾음
}
int main() {
HashTable *ht = createHashTable();
htSet(ht, "apple", 100);
htSet(ht, "banana", 200);
htSet(ht, "cherry", 300);
int val;
if (htGet(ht, "banana", &val))
printf("banana: %d\n", val); // banana: 200
return 0;
}
임베디드 시스템이나 네트워크 프로토콜 구현할 때 진짜 중요한 기법임.
비트 필드를 쓰면 비트 단위로 메모리를 제어할 수 있어서 메모리가 극도로 제한된 환경에서 빛을 발함 ✨
// 일반 구조체로 플래그 관리 (비효율적)
typedef struct {
int isActive; // 4 bytes (0 또는 1만 저장하는데 4 bytes 낭비!)
int isAdmin; // 4 bytes
int isVerified; // 4 bytes
int isOnline; // 4 bytes
} UserFlags_Bad; // 총 16 bytes
// 비트 필드로 최적화 (효율적!)
typedef struct {
unsigned int isActive : 1; // 1 bit
unsigned int isAdmin : 1; // 1 bit
unsigned int isVerified : 1; // 1 bit
unsigned int isOnline : 1; // 1 bit
unsigned int userLevel : 4; // 4 bits (0~15 레벨)
unsigned int reserved : 24; // 나머지 비트 (패딩)
} UserFlags_Good; // 총 4 bytes (75% 절약!)
int main() {
printf("나쁜 방식: %zu bytes\n", sizeof(UserFlags_Bad)); // 16 bytes
printf("좋은 방식: %zu bytes\n", sizeof(UserFlags_Good)); // 4 bytes
UserFlags_Good flags = {0};
flags.isActive = 1;
flags.isAdmin = 0;
flags.isVerified = 1;
flags.userLevel = 7; // 레벨 7
printf("활성: %d, 관리자: %d, 인증: %d, 레벨: %d\n",
flags.isActive, flags.isAdmin,
flags.isVerified, flags.userLevel);
return 0;
}
// ARM Cortex-M GPIO 레지스터 구조 예시
typedef struct {
unsigned int MODER : 2; // 모드 설정 (00:입력, 01:출력, 10:AF, 11:아날로그)
unsigned int OTYPER : 1; // 출력 타입 (0:푸시풀, 1:오픈드레인)
unsigned int OSPEEDR : 2; // 속도 설정
unsigned int PUPDR : 2; // 풀업/풀다운
unsigned int IDR : 1; // 입력 데이터
unsigned int ODR : 1; // 출력 데이터
unsigned int reserved: 23; // 예약
} GPIO_Pin;
// 실제 하드웨어 레지스터에 맵핑
#define GPIOA_BASE 0x40020000
#define GPIOA_MODER (*((volatile uint32_t*)(GPIOA_BASE + 0x00)))
// 공용체로 레지스터 비트 접근 편리하게
typedef union {
uint32_t reg; // 전체 레지스터 값
struct {
unsigned int pin0 : 2;
unsigned int pin1 : 2;
unsigned int pin2 : 2;
// ... 16개 핀
} bits;
} GPIO_MODER_t;
// 사용 예시
void configureGPIO() {
GPIO_MODER_t moder;
moder.reg = GPIOA_MODER; // 현재 레지스터 값 읽기
moder.bits.pin5 = 0b01; // PA5를 출력 모드로 설정
GPIOA_MODER = moder.reg; // 레지스터에 쓰기
}
1. 비트 필드의 순서와 패킹 방식은 컴파일러/플랫폼마다 다를 수 있음
2. 이식성이 중요한 코드에서는 비트 연산(|, &, ~, <<, >>)을 직접 쓰는 게 더 안전함
3. 비트 필드 멤버의 주소를 취할 수 없음 (& 연산자 사용 불가)
이제 진짜 고급 내용임 ㅋㅋ 구조체에 함수 포인터를 넣으면 C에서도 객체지향 프로그래밍의 다형성(Polymorphism)을 구현할 수 있음.
C++ vtable의 원리가 바로 이거임! 리눅스 커널, SQLite, CPython 같은 거대한 C 프로젝트들이 이 패턴을 적극 활용함 🔥
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// "인터페이스" 역할을 하는 구조체
typedef struct Shape Shape;
struct Shape {
// 함수 포인터 (가상 함수 테이블 역할)
double (*area)(const Shape *self);
double (*perimeter)(const Shape *self);
void (*print)(const Shape *self);
void (*destroy)(Shape *self);
// 공통 데이터
char name[32];
};
// 원(Circle) 구현
typedef struct {
Shape base; // 반드시 첫 번째 멤버!
double radius;
} Circle;
double circle_area(const Shape *self) {
Circle *c = (Circle*)self;
return 3.14159265 * c->radius * c->radius;
}
double circle_perimeter(const Shape *self) {
Circle *c = (Circle*)self;
return 2 * 3.14159265 * c->radius;
}
void circle_print(const Shape *self) {
Circle *c = (Circle*)self;
printf("원 (반지름: %.2f)\n", c->radius);
}
void circle_destroy(Shape *self) { free(self); }
Shape* createCircle(double radius) {
Circle *c = (Circle*)malloc(sizeof(Circle));
// 함수 포인터 설정 (vtable 초기화)
c->base.area = circle_area;
c->base.perimeter = circle_perimeter;
c->base.print = circle_print;
c->base.destroy = circle_destroy;
strcpy(c->base.name, "원");
c->radius = radius;
return (Shape*)c;
}
// 직사각형(Rectangle) 구현
typedef struct {
Shape base;
double width, height;
} Rectangle;
double rect_area(const Shape *self) {
Rectangle *r = (Rectangle*)self;
return r->width * r->height;
}
double rect_perimeter(const Shape *self) {
Rectangle *r = (Rectangle*)self;
return 2 * (r->width + r->height);
}
void rect_print(const Shape *self) {
Rectangle *r = (Rectangle*)self;
printf("직사각형 (%.2f x %.2f)\n", r->width, r->height);
}
void rect_destroy(Shape *self) { free(self); }
Shape* createRectangle(double w, double h) {
Rectangle *r = (Rectangle*)malloc(sizeof(Rectangle));
r->base.area = rect_area;
r->base.perimeter = rect_perimeter;
r->base.print = rect_print;
r->base.destroy = rect_destroy;
strcpy(r->base.name, "직사각형");
r->width = w;
r->height = h;
return (Shape*)r;
}
// 다형성 활용! Shape* 배열로 다양한 도형 처리
int main() {
Shape *shapes[3];
shapes[0] = createCircle(5.0);
shapes[1] = createRectangle(4.0, 6.0);
shapes[2] = createCircle(3.0);
printf("=== 도형 정보 ===\n");
for (int i = 0; i < 3; i++) {
shapes[i]->print(shapes[i]); // 다형적 호출!
printf(" 넓이: %.2f\n", shapes[i]->area(shapes[i]));
printf(" 둘레: %.2f\n", shapes[i]->perimeter(shapes[i]));
}
// 메모리 해제
for (int i = 0; i < 3; i++)
shapes[i]->destroy(shapes[i]);
return 0;
}
이 패턴이 바로 C언어로 OOP 구현하기의 핵심임.
리눅스 커널의 file_operations 구조체, GLib의 GObject 시스템이 정확히 이 방식으로 동작함.
재능넷에서 C언어 관련 재능을 찾아보면 이런 고급 패턴을 다루는 전문가들도 있으니 참고해봐도 좋음 😊
// 이벤트 핸들러 시스템
typedef void (*EventHandler)(void *data, void *context);
typedef struct {
EventHandler handler;
void *context;
} EventListener;
typedef struct {
EventListener listeners[16];
int count;
char eventName[32];
} EventEmitter;
void addListener(EventEmitter *em, EventHandler handler, void *ctx) {
if (em->count < 16) {
em->listeners[em->count].handler = handler;
em->listeners[em->count].context = ctx;
em->count++;
}
}
void emit(EventEmitter *em, void *data) {
for (int i = 0; i < em->count; i++) {
em->listeners[i].handler(data, em->listeners[i].context);
}
}
// 사용 예시
void onUserLogin(void *data, void *context) {
char *username = (char*)data;
printf("로그인 이벤트: %s\n", username);
}
void onUserLoginLog(void *data, void *context) {
char *username = (char*)data;
printf("로그 기록: %s 로그인\n", username);
}
int main() {
EventEmitter loginEvent = {.count=0, .eventName="login"};
addListener(&loginEvent, onUserLogin, NULL);
addListener(&loginEvent, onUserLoginLog, NULL);
emit(&loginEvent, "김철수");
// 출력:
// 로그인 이벤트: 김철수
// 로그 기록: 김철수 로그인
return 0;
}
여기서부터는 진짜 실수하기 쉬운 것들 모음임 ㅋㅋ 이거 모르면 나중에 디버깅하다가 머리 뽑힘 😅
typedef struct {
int id;
char *name; // 동적 할당된 포인터!
} Person;
int main() {
Person p1;
p1.id = 1;
p1.name = (char*)malloc(20);
strcpy(p1.name, "김철수");
// ⚠️ 얕은 복사 (위험!)
Person p2 = p1; // name 포인터만 복사됨!
// p1과 p2가 같은 메모리를 가리킴
// p2.name을 수정하면 p1.name도 바뀜!
strcpy(p2.name, "이영희");
printf("p1.name: %s\n", p1.name); // 이영희 (의도치 않은 변경!)
// 더 심각한 문제: 둘 다 free하면 double free 에러!
free(p1.name);
free(p2.name); // 💥 이미 해제된 메모리 다시 해제!
return 0;
}
// ✅ 올바른 깊은 복사 함수
Person copyPerson(const Person *src) {
Person dst;
dst.id = src->id;
dst.name = (char*)malloc(strlen(src->name) + 1);
strcpy(dst.name, src->name); // 새 메모리에 복사
return dst;
}
typedef struct {
int x;
int y;
} Point;
int main() {
Point p1 = {1, 2};
Point p2 = {1, 2};
// ⚠️ 이렇게 하면 안 됨!
if (p1 == p2) { // 컴파일 에러! 구조체는 == 비교 불가
printf("같음\n");
}
// ✅ memcmp 사용 (패딩이 없을 때만 안전)
if (memcmp(&p1, &p2, sizeof(Point)) == 0) {
printf("같음\n");
}
// ✅ 가장 안전한 방법: 직접 비교 함수 작성
// (패딩 바이트 문제 없음)
return 0;
}
// 안전한 비교 함수
int pointEquals(const Point *a, const Point *b) {
return a->x == b->x && a->y == b->y;
}
// C99 이후 지원되는 가변 길이 배열 멤버
typedef struct {
int size;
int data[]; // 가변 길이 배열 멤버 (반드시 마지막에!)
} DynamicArray;
// 사용법
DynamicArray* createArray(int n) {
// sizeof(DynamicArray) + n개의 int 크기만큼 할당
DynamicArray *arr = (DynamicArray*)malloc(
sizeof(DynamicArray) + n * sizeof(int)
);
arr->size = n;
return arr;
}
int main() {
DynamicArray *arr = createArray(5);
for (int i = 0; i < arr->size; i++) {
arr->data[i] = i * 10;
}
for (int i = 0; i < arr->size; i++) {
printf("%d ", arr->data[i]); // 0 10 20 30 40
}
free(arr);
return 0;
}
// ⚠️ 주의: 가변 길이 배열 멤버가 있는 구조체는
// 스택에 선언하거나, 배열로 만들거나, 복사할 수 없음!
// 상호 참조 구조체 (서로를 참조하는 경우)
// ⚠️ 이렇게 하면 컴파일 에러!
typedef struct {
int data;
Child *child; // Child가 아직 정의 안 됨!
} Parent;
typedef struct {
int data;
Parent *parent;
} Child;
// ✅ 전방 선언으로 해결
typedef struct Child Child; // 전방 선언
typedef struct {
int data;
Child *child; // 포인터는 불완전 타입도 OK
} Parent;
struct Child { // 실제 정의
int data;
Parent *parent;
};
지금까지 배운 내용을 종합해서 미니 데이터베이스 엔진을 설계해보자.
이 예제는 구조체, 공용체, 함수 포인터, 동적 메모리를 모두 활용함 💪
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// ===== 컬럼 타입 정의 =====
typedef enum {
COL_INT,
COL_FLOAT,
COL_TEXT
} ColumnType;
// ===== 셀 값 (태그된 공용체) =====
typedef struct {
ColumnType type;
union {
int iVal;
float fVal;
char tVal[256];
} value;
} Cell;
// ===== 컬럼 스키마 =====
typedef struct {
char name[64];
ColumnType type;
int nullable;
} ColumnDef;
// ===== 행(Row) =====
typedef struct Row {
Cell *cells;
struct Row *next;
} Row;
// ===== 테이블 =====
typedef struct {
char name[64];
ColumnDef *columns;
int colCount;
Row *head;
int rowCount;
// 함수 포인터로 연산 정의
int (*insert)(void *table, Cell *cells);
void (*print)(void *table);
void (*destroy)(void *table);
} Table;
// ===== 셀 생성 헬퍼 =====
Cell makeIntCell(int val) {
Cell c = {.type = COL_INT};
c.value.iVal = val;
return c;
}
Cell makeFloatCell(float val) {
Cell c = {.type = COL_FLOAT};
c.value.fVal = val;
return c;
}
Cell makeTextCell(const char *val) {
Cell c = {.type = COL_TEXT};
strncpy(c.value.tVal, val, 255);
return c;
}
// ===== 셀 출력 =====
void printCell(const Cell *c) {
switch (c->type) {
case COL_INT: printf("%-12d", c->value.iVal); break;
case COL_FLOAT: printf("%-12.2f", c->value.fVal); break;
case COL_TEXT: printf("%-12s", c->value.tVal); break;
}
}
// ===== 테이블 삽입 =====
int tableInsert(void *tbl, Cell *cells) {
Table *t = (Table*)tbl;
Row *newRow = (Row*)malloc(sizeof(Row));
newRow->cells = (Cell*)malloc(t->colCount * sizeof(Cell));
memcpy(newRow->cells, cells, t->colCount * sizeof(Cell));
newRow->next = NULL;
// 끝에 추가
if (t->head == NULL) {
t->head = newRow;
} else {
Row *curr = t->head;
while (curr->next) curr = curr->next;
curr->next = newRow;
}
t->rowCount++;
return 1;
}
// ===== 테이블 출력 =====
void tablePrint(void *tbl) {
Table *t = (Table*)tbl;
printf("\n테이블: %s (%d행)\n", t->name, t->rowCount);
printf("%-4s", "");
for (int i = 0; i < t->colCount; i++)
printf("%-12s", t->columns[i].name);
printf("\n");
// 구분선
for (int i = 0; i < 4 + t->colCount * 12; i++) printf("-");
printf("\n");
Row *curr = t->head;
int rowNum = 1;
while (curr != NULL) {
printf("%-4d", rowNum++);
for (int i = 0; i < t->colCount; i++)
printCell(&curr->cells[i]);
printf("\n");
curr = curr->next;
}
}
// ===== 테이블 해제 =====
void tableDestroy(void *tbl) {
Table *t = (Table*)tbl;
Row *curr = t->head;
while (curr) {
Row *next = curr->next;
free(curr->cells);
free(curr);
curr = next;
}
free(t->columns);
free(t);
}
// ===== 테이블 생성 =====
Table* createTable(const char *name, ColumnDef *cols, int colCount) {
Table *t = (Table*)malloc(sizeof(Table));
strncpy(t->name, name, 63);
t->columns = (ColumnDef*)malloc(colCount * sizeof(ColumnDef));
memcpy(t->columns, cols, colCount * sizeof(ColumnDef));
t->colCount = colCount;
t->head = NULL;
t->rowCount = 0;
// 함수 포인터 설정
t->insert = tableInsert;
t->print = tablePrint;
t->destroy = tableDestroy;
return t;
}
// ===== 메인 =====
int main() {
// 스키마 정의
ColumnDef cols[] = {
{"id", COL_INT, 0},
{"name", COL_TEXT, 0},
{"score", COL_FLOAT, 1}
};
Table *students = createTable("students", cols, 3);
// 데이터 삽입
Cell row1[] = {makeIntCell(1), makeTextCell("김철수"), makeFloatCell(95.5f)};
Cell row2[] = {makeIntCell(2), makeTextCell("이영희"), makeFloatCell(88.0f)};
Cell row3[] = {makeIntCell(3), makeTextCell("박민준"), makeFloatCell(92.3f)};
students->insert(students, row1);
students->insert(students, row2);
students->insert(students, row3);
// 출력
students->print(students);
// 해제
students->destroy(students);
return 0;
}
현대 CPU는 메모리를 캐시 라인(보통 64 bytes) 단위로 읽음.
구조체 설계가 캐시 효율에 직접적인 영향을 미치는데, 이걸 모르면 성능이 10배 이상 차이날 수 있음 😱
#include <stdio.h>
#include <time.h>
#define N 1000000
// AoS (Array of Structures) - 일반적인 방식
typedef struct {
float x, y, z; // 위치
float vx, vy, vz; // 속도
float mass; // 질량
} Particle_AoS;
// SoA (Structure of Arrays) - 캐시 최적화 방식
typedef struct {
float *x, *y, *z; // 위치 배열
float *vx, *vy, *vz; // 속도 배열
float *mass; // 질량 배열
int count;
} Particles_SoA;
// AoS 방식: x 좌표만 업데이트
// 메모리 접근 패턴: x, [y,z,vx,vy,vz,mass 건너뜀], x, [건너뜀], ...
// 캐시 미스 많이 발생!
void updateX_AoS(Particle_AoS *particles, int n) {
for (int i = 0; i < n; i++) {
particles[i].x += particles[i].vx; // 28 bytes 간격으로 접근
}
}
// SoA 방식: x 좌표만 업데이트
// 메모리 접근 패턴: x[0], x[1], x[2], ... (연속!)
// 캐시 히트율 매우 높음!
void updateX_SoA(Particles_SoA *p) {
for (int i = 0; i < p->count; i++) {
p->x[i] += p->vx[i]; // 4 bytes 간격으로 연속 접근
}
}
// 벤치마크 결과 (일반적으로):
// AoS: ~50ms
// SoA: ~15ms (3배 이상 빠름!)
// 이유: SoA는 캐시 라인 하나에 16개의 float 값이 들어감
// 멀티스레드 환경에서 False Sharing 문제
// 두 스레드가 같은 캐시 라인의 다른 변수를 수정할 때 성능 저하
// ⚠️ 나쁜 설계 (False Sharing 발생)
typedef struct {
int counter1; // 스레드 1이 수정
int counter2; // 스레드 2가 수정
// 두 변수가 같은 캐시 라인(64 bytes)에 있음!
} BadCounters;
// ✅ 좋은 설계 (캐시 라인 패딩으로 False Sharing 방지)
typedef struct {
int counter;
char padding[60]; // 캐시 라인 크기(64) - sizeof(int)(4) = 60 bytes 패딩
} CacheAlignedCounter;
// 또는 컴파일러 지시어 사용
typedef struct {
int counter;
} __attribute__((aligned(64))) AlignedCounter;
// 멀티스레드 카운터 배열
CacheAlignedCounter threadCounters[8]; // 각 스레드가 독립된 캐시 라인 사용
| 최적화 기법 | 효과 | 적용 상황 |
|---|---|---|
| 멤버 순서 최적화 | 메모리 25~50% 절약 | 메모리 제한 환경 |
| SoA 패턴 | 처리 속도 2~5배 향상 | 대량 데이터 일괄 처리 |
| 캐시 라인 정렬 | 멀티스레드 성능 향상 | 병렬 처리 시스템 |
| 비트 필드 | 메모리 최대 32배 절약 | 플래그, 임베디드 시스템 |
| #pragma pack | 패딩 제거 | 네트워크, 파일 포맷 |
자 이제 마무리임 ㅋㅋ 오늘 배운 내용을 한 번에 정리해보자.
메모리 정렬 패딩 최적화 포인터 활용 동적 할당 중첩 구조체
• 멤버 순서를 큰 타입 → 작은 타입으로 배치해서 패딩 최소화
• typedef로 타입 별칭 지정해서 코드 가독성 향상
• 함수에 넘길 때는 포인터로 넘겨서 불필요한 복사 방지
• 동적 포인터 멤버는 반드시 생성/소멸 함수 쌍으로 관리
• 함수 포인터 멤버로 C에서도 OOP 패턴 구현 가능
메모리 공유 타입 펀닝 태그된 공용체 네트워크 파싱
• 모든 멤버가 같은 메모리 공간 공유, 크기는 가장 큰 멤버 기준
• 마지막에 쓴 멤버만 유효한 값을 가짐
• 태그 필드와 함께 써서 다형적 데이터 처리 (태그된 공용체)
• 비트 패턴 분석, 네트워크 패킷 파싱에 강력함
• 비트 필드와 조합하면 하드웨어 레지스터 맵핑에 최적
캐시 최적화 SoA 패턴 비트 필드 False Sharing
• 대량 데이터 처리 시 SoA(Structure of Arrays) 패턴 고려
• 멀티스레드 환경에서 캐시 라인 패딩으로 False Sharing 방지
• 플래그 변수는 비트 필드로 메모리 최대 32배 절약
• #pragma pack은 네트워크/파일 포맷에서만 신중하게 사용
C언어에서 구조체와 공용체는 단순한 데이터 묶음 이상의 의미를 가짐.
제대로 설계된 구조체는 코드의 가독성, 유지보수성, 성능을 동시에 잡을 수 있는 강력한 도구임.
리눅스 커널, SQLite, Redis, CPython 같은 세계적인 오픈소스 프로젝트들이 모두 C언어의 구조체 기반 설계 위에서 동작하고 있다는 사실을 잊지 말자 🔥
이 내용들을 실제로 코딩하면서 익히는 게 제일 중요함. 이론만 알고 코드 안 짜면 금방 까먹음 ㅋㅋ
직접 연결 리스트도 만들어보고, 해시 테이블도 구현해보고, 함수 포인터로 OOP 패턴도 짜봐야 진짜 실력이 됨 💪
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.