콘텐츠 대표 이미지 - C언어 구조체와 공용체를 활용한 효율적 데이터 구조 설계 전략
📘 C언어 · 프로그램개발

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

struct & union, 제대로 알고 쓰면 진짜 레전드 코드 나옴 🔥

야 솔직히 말해서, C언어 배울 때 포인터 다음으로 사람들이 제일 헷갈려하는 게 바로 구조체(struct)랑 공용체(union)임 ㅋㅋㅋ
"아 그냥 변수 여러 개 묶는 거 아니야?" 라고 생각하는 순간... 그건 진짜 빙산의 일각만 본 거임.
이 두 가지를 제대로 이해하고 설계에 활용하면, 메모리 효율이 극적으로 올라가고 코드 가독성도 미쳐버림 🚀

오늘은 그냥 문법 설명 말고, 실제로 어떻게 데이터 구조를 설계하는지 팩트 위주로 빡세게 파봄.


구조체 vs 공용체 메모리 구조 비교 struct (구조체) 각 멤버가 독립된 메모리 공간 차지 int age 4 bytes float score 4 bytes char name[20] 20 bytes 총 합계: 28 bytes ⚡ union (공용체) 모든 멤버가 같은 메모리 공간 공유 int age (4 bytes) ↕ 같은 공간 공유 float score (4 bytes) ↕ 같은 공간 공유 char name[20] (20 bytes) ← 가장 큰 멤버 크기 기준 총 합계: 20 bytes 💡 같은 멤버 구성이라도 union은 메모리를 훨씬 적게 씀!
🏗️ 1. 구조체(struct) 기초부터 제대로 파기

구조체는 서로 다른 타입의 변수들을 하나의 논리적 단위로 묶는 사용자 정의 타입임.
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 키워드 없이 바로 사용 가능!
💡 실무 팁: typedef를 쓰면 매번 struct 키워드를 안 써도 돼서 코드가 훨씬 깔끔해짐. 헤더 파일에 typedef로 정의해두는 게 업계 관행임.
📌 구조체 초기화 방법들
// 방법 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)가 진짜 꿀임 ㅋㅋ
멤버 순서 바꿔도 안전하고, 어떤 멤버에 뭘 넣는지 코드만 봐도 바로 알 수 있어서 유지보수할 때 개편함 👍

📌 구조체 메모리 레이아웃과 패딩(Padding)

여기서부터 진짜 중요한 내용 나옴. 많은 사람들이 모르는 부분인데, 구조체는 메모리 정렬(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;
}
⚠️ 패딩 주의! BadLayout은 이론상 6 bytes지만 실제로는 12 bytes임. CPU가 int를 4바이트 경계에 맞춰 읽으려고 컴파일러가 자동으로 패딩을 삽입하기 때문. 멤버 순서만 잘 배치해도 메모리를 33% 절약할 수 있음!

이게 바로 구조체 멤버 배치 최적화의 핵심임. 규칙은 간단함:
큰 타입 → 작은 타입 순서로 배치하면 패딩이 최소화됨.

타입크기정렬 요구사항
char1 byte1 byte 경계
short2 bytes2 byte 경계
int4 bytes4 byte 경계
long long8 bytes8 byte 경계
float4 bytes4 byte 경계
double8 bytes8 byte 경계
📌 #pragma pack으로 패딩 제어하기
// 패딩 없이 꽉꽉 채워서 저장 (네트워크 프로토콜, 파일 포맷 등에서 사용)
#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;
💡 언제 pack 쓰냐? 네트워크 패킷 파싱, 바이너리 파일 포맷 처리, 임베디드 시스템에서 레지스터 맵 정의할 때 씀. 근데 성능은 약간 떨어질 수 있으니까 꼭 필요한 경우에만!

🔗 2. 구조체 포인터와 동적 메모리 활용

구조체를 진짜 잘 쓰려면 포인터랑 같이 써야 함. 함수에 구조체를 넘길 때 값 복사로 넘기면 메모리 낭비가 심하거든. 특히 구조체가 크면 클수록 더 심각함 😱

📌 구조체 포인터 기본
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;
}
💡 C언어의 OOP 흉내내기! createStudent()는 생성자, destroyStudent()는 소멸자 역할임. C++의 new/delete를 C에서 구현하는 패턴이고, 실제 임베디드 시스템이나 OS 커널 코드에서 이런 패턴 진짜 많이 씀.
📌 중첩 구조체 (Nested Struct)
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;
}

구조체 멤버 배치 최적화 전략 같은 멤버, 다른 순서 → 메모리 크기가 달라짐! ❌ 나쁜 배치 (12 bytes) char a 패딩 3 bytes 낭비! int b 4 bytes char c 패딩 3 bytes 낭비! 1B +3B 4B 1B +3B ↓ 멤버 순서 재배치 ✅ 좋은 배치 (8 bytes) int b 4 bytes char a char c 패딩 2B 4B 1B 1B +2B 💡 핵심 규칙: 큰 타입 먼저, 작은 타입 나중에! double → long long → int → short → char 순서로 배치하면 패딩 최소화
🔀 3. 공용체(union) 완전 정복

공용체는 구조체랑 문법이 비슷한데, 동작 방식이 완전히 다름.
모든 멤버가 같은 메모리 공간을 공유하고, 크기는 가장 큰 멤버 기준으로 결정됨.
"그럼 동시에 여러 멤버 값 저장 못하는 거 아니야?" 맞음! 그게 바로 공용체의 특성이고, 이걸 잘 활용하는 게 포인트임 ㅋㅋ

📌 공용체 기본 사용법
#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;
}
📌 공용체의 실전 활용 1: 타입 변환 트릭

공용체의 가장 유명한 활용법 중 하나가 바로 타입 펀닝(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;
}
💡 비트 필드(Bit Field)! 구조체 멤버 뒤에 : 숫자를 붙이면 그 멤버가 차지하는 비트 수를 지정할 수 있음. 위 예시처럼 float의 내부 구조를 분석하거나, 하드웨어 레지스터 맵을 정의할 때 진짜 유용함.
📌 공용체의 실전 활용 2: 다형적 데이터 처리

이게 진짜 공용체의 킬러 앱임 ㅋㅋ 여러 타입의 데이터를 하나의 구조로 처리할 때 씀.

// 다양한 타입의 값을 하나의 구조체로 처리
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 파서, 스크립팅 언어 인터프리터, 데이터베이스 엔진 등에서 핵심 패턴으로 쓰임 🔥

📌 공용체의 실전 활용 3: 네트워크 패킷 처리
// 네트워크 패킷 헤더 파싱 예시
#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;
    }
}

🌲 4. 구조체로 만드는 자료구조들

이제 진짜 실전임 ㅋㅋ 구조체를 활용해서 실제 자료구조를 구현해보자.
이걸 이해하면 C언어로 뭐든 만들 수 있다는 자신감이 생김 💪

📌 연결 리스트 (Linked List)

구조체의 자기 참조(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;
}
📌 스택 (Stack)
#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;
}
📌 이진 트리 (Binary Tree)
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;
}
📌 해시 테이블 (Hash Table)
#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;
}

구조체 기반 자료구조 선택 가이드 연결 리스트 ✅ 삽입/삭제 O(1) ✅ 크기 동적 변경 ❌ 탐색 O(n) ❌ 포인터 오버헤드 💡 삽입/삭제 빈번할 때 큐, 스택 구현에 적합 스택 / 큐 ✅ push/pop O(1) ✅ 구현 단순 ❌ 중간 접근 불가 ❌ 크기 제한 있음 💡 함수 호출 스택 괄호 검사, BFS/DFS 이진 탐색 트리 ✅ 탐색 O(log n) ✅ 정렬 유지 ❌ 최악 O(n) 가능 ❌ 구현 복잡 💡 정렬된 데이터 범위 검색에 적합 해시 테이블 ✅ 탐색 O(1) 평균 ✅ 키-값 저장 ❌ 메모리 낭비 ❌ 충돌 처리 필요 💡 빠른 조회 필요 캐시, 딕셔너리 구현 🎯 자료구조 선택 기준 삽입/삭제 빈번 → 연결 리스트 LIFO/FIFO 패턴 → 스택/큐 정렬된 탐색 필요 → 이진 탐색 트리 빠른 키 조회 → 해시 테이블 모든 자료구조의 기반 = C언어 구조체(struct) 🏗️ 구조체를 잘 설계하면 어떤 자료구조든 효율적으로 구현 가능! 재능넷 같은 플랫폼의 백엔드 시스템도 이런 자료구조 위에서 동작함
⚡ 5. 비트 필드(Bit Field)로 메모리 극한 최적화

임베디드 시스템이나 네트워크 프로토콜 구현할 때 진짜 중요한 기법임.
비트 필드를 쓰면 비트 단위로 메모리를 제어할 수 있어서 메모리가 극도로 제한된 환경에서 빛을 발함 ✨

📌 비트 필드 기본 사용법
// 일반 구조체로 플래그 관리 (비효율적)
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. 비트 필드 멤버의 주소를 취할 수 없음 (& 연산자 사용 불가)

🎨 6. 고급 패턴: 함수 포인터와 구조체의 조합

이제 진짜 고급 내용임 ㅋㅋ 구조체에 함수 포인터를 넣으면 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언어 관련 재능을 찾아보면 이런 고급 패턴을 다루는 전문가들도 있으니 참고해봐도 좋음 😊

📌 콜백 패턴 (Callback Pattern)
// 이벤트 핸들러 시스템
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;
}

🛡️ 7. 구조체 설계 시 주의해야 할 함정들

여기서부터는 진짜 실수하기 쉬운 것들 모음임 ㅋㅋ 이거 모르면 나중에 디버깅하다가 머리 뽑힘 😅

📌 함정 1: 얕은 복사 vs 깊은 복사
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;
}
📌 함정 2: 구조체 비교는 == 안 됨
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;
}
📌 함정 3: 가변 길이 배열 멤버 (Flexible Array Member)
// 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;
}

// ⚠️ 주의: 가변 길이 배열 멤버가 있는 구조체는
// 스택에 선언하거나, 배열로 만들거나, 복사할 수 없음!
📌 함정 4: 전방 선언과 불완전 타입
// 상호 참조 구조체 (서로를 참조하는 경우)
// ⚠️ 이렇게 하면 컴파일 에러!
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;
};
💡 불완전 타입(Incomplete Type): 포인터는 가리키는 타입의 크기를 몰라도 선언 가능함. 포인터 자체의 크기는 항상 고정(32비트: 4bytes, 64비트: 8bytes)이기 때문. 이걸 이용해서 상호 참조 구조체를 구현할 수 있음.

🚀 8. 실전 프로젝트: 간단한 데이터베이스 엔진 설계

지금까지 배운 내용을 종합해서 미니 데이터베이스 엔진을 설계해보자.
이 예제는 구조체, 공용체, 함수 포인터, 동적 메모리를 모두 활용함 💪

#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;
}
💡 이 미니 DB 엔진은 SQLite의 내부 구조와 유사한 패턴을 사용함. 실제 SQLite도 C언어로 작성되어 있고, 이런 구조체 기반 설계 패턴이 핵심임. 재능넷에서 C언어 프로젝트 의뢰할 때 이런 수준의 설계 능력을 요구하는 경우도 있으니 참고!

📊 9. 성능 최적화: 캐시 친화적 구조체 설계

현대 CPU는 메모리를 캐시 라인(보통 64 bytes) 단위로 읽음.
구조체 설계가 캐시 효율에 직접적인 영향을 미치는데, 이걸 모르면 성능이 10배 이상 차이날 수 있음 😱

📌 AoS vs SoA (Array of Structures vs Structure of Arrays)
#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 문제
// 두 스레드가 같은 캐시 라인의 다른 변수를 수정할 때 성능 저하

// ⚠️ 나쁜 설계 (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패딩 제거네트워크, 파일 포맷

C언어 구조체 설계 체크리스트 ✅ 🏗️ 설계 단계 ✅ 멤버를 큰 타입 → 작은 타입 순서로 배치 ✅ typedef로 타입 별칭 지정 ✅ 동적 포인터 멤버는 생성/소멸 함수 쌍으로 ✅ 네트워크/파일 구조체는 #pragma pack 적용 ✅ 공용체 사용 시 태그 필드로 현재 타입 추적 ✅ sizeof()로 실제 크기 항상 확인 ⚠️ 주의 사항 ❌ 구조체 직접 == 비교 금지 ❌ 포인터 멤버 얕은 복사 주의 ❌ 비트 필드 주소 취하기 불가 ❌ 가변 배열 멤버 스택 선언 불가 ❌ 공용체 멤버 동시 유효 불가 ❌ 비트 필드 이식성 의존 금지 💡 이 체크리스트만 지켜도 C언어 구조체 설계 레벨업 완료!
🎯 10. 구조체와 공용체 활용 총정리

자 이제 마무리임 ㅋㅋ 오늘 배운 내용을 한 번에 정리해보자.

📌 구조체(struct) 핵심 요약

메모리 정렬 패딩 최적화 포인터 활용 동적 할당 중첩 구조체

• 멤버 순서를 큰 타입 → 작은 타입으로 배치해서 패딩 최소화
• typedef로 타입 별칭 지정해서 코드 가독성 향상
• 함수에 넘길 때는 포인터로 넘겨서 불필요한 복사 방지
• 동적 포인터 멤버는 반드시 생성/소멸 함수 쌍으로 관리
• 함수 포인터 멤버로 C에서도 OOP 패턴 구현 가능
📌 공용체(union) 핵심 요약

메모리 공유 타입 펀닝 태그된 공용체 네트워크 파싱

• 모든 멤버가 같은 메모리 공간 공유, 크기는 가장 큰 멤버 기준
• 마지막에 쓴 멤버만 유효한 값을 가짐
• 태그 필드와 함께 써서 다형적 데이터 처리 (태그된 공용체)
• 비트 패턴 분석, 네트워크 패킷 파싱에 강력함
• 비트 필드와 조합하면 하드웨어 레지스터 맵핑에 최적
📌 성능 최적화 핵심 요약

캐시 최적화 SoA 패턴 비트 필드 False Sharing

• 대량 데이터 처리 시 SoA(Structure of Arrays) 패턴 고려
• 멀티스레드 환경에서 캐시 라인 패딩으로 False Sharing 방지
• 플래그 변수는 비트 필드로 메모리 최대 32배 절약
• #pragma pack은 네트워크/파일 포맷에서만 신중하게 사용

C언어에서 구조체와 공용체는 단순한 데이터 묶음 이상의 의미를 가짐.
제대로 설계된 구조체는 코드의 가독성, 유지보수성, 성능을 동시에 잡을 수 있는 강력한 도구임.
리눅스 커널, SQLite, Redis, CPython 같은 세계적인 오픈소스 프로젝트들이 모두 C언어의 구조체 기반 설계 위에서 동작하고 있다는 사실을 잊지 말자 🔥

이 내용들을 실제로 코딩하면서 익히는 게 제일 중요함. 이론만 알고 코드 안 짜면 금방 까먹음 ㅋㅋ
직접 연결 리스트도 만들어보고, 해시 테이블도 구현해보고, 함수 포인터로 OOP 패턴도 짜봐야 진짜 실력이 됨 💪

댓글 작성

이 글에 대한 여러분의 생각을 들려주세요

댓글 0
댓글을 불러오는 중입니다.

아직 댓글이 없습니다.