STUDY

C 언어 핵심 개념 요약 + 예시

Lim임 2025. 11. 10. 01:44

C 언어 핵심 개념 요약 + 예시

각 주제마다

  • 개념/필요성
  • C 예시

를 제공합니다.


1) 함수 포인터

  • 개념/필요성
    • “함수를 가리키는 포인터”. 실행 중(런타임)에 어떤 함수를 호출할지 선택할 수 있어 콜백, 전략 교체, 상태 머신, 플러그인 등에 유용합니다.
    • 정적 바인딩: 어떤 함수가 호출될지 컴파일 타임에 고정.
    • 동적 바인딩: 어떤 함수를 호출할지 런타임에 결정(함수 포인터가 핵심 수단).
    • 흔한 오해 바로잡기
      • “주소값을 가져와 쓰기 때문에 실제 값이 변한다/원본을 참조한다”는 설명은 ‘데이터 포인터’에 해당합니다. 함수 포인터는 “함수의 주소”를 보유할 뿐 데이터를 직접 바꾸지 않습니다. 다만 호출 대상 함수를 바꿔 “동작 결과”를 바꿀 수는 있습니다.
      • “메모리를 줄이려고 쓴다”는 것도 주목적은 아닙니다. 주 목적은 유연성과 다형성(런타임 선택)이며, 간혹 테이블 주도 설계에서 분기 비용/테이블 크기와 관련된 트레이드오프가 있을 뿐입니다.
#include <stdio.h>
#include <stdlib.h>

/* 전략(Strategy) 함수들 */
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

/* 함수 포인터로 동적 바인딩 */
int apply(int (*op)(int,int), int x, int y) {
    return op(x, y);
}

/* 표준 라이브러리 콜백(qsort) 예시 */
int cmp_int_asc(const void *a, const void *b) {
    int x = *(const int*)a, y = *(const int*)b;
    return (x > y) - (x < y);
}

int main() {
    printf("%d\n", apply(add, 3, 5)); // 8
    printf("%d\n", apply(sub, 3, 5)); // -2

    int arr[] = {4,1,3,2};
    qsort(arr, 4, sizeof(int), cmp_int_asc);
    for (int i = 0; i < 4; i++) printf("%d ", arr[i]); // 1 2 3 4
    return 0;
}

2) 구조체(struct) – “변수의 집”, 클래스 유사 추상화

  • 개념/필요성
    • 서로 연관된 여러 변수를 하나의 타입으로 묶어 가독성과 모듈성을 높입니다.
    • 직접 접근: . / 간접 접근(포인터): ->
    • C에는 class가 없지만, struct + 함수(필요시 함수 포인터)로 “클래스 비슷한 추상화”가 가능합니다.
#include <stdio.h>

typedef struct {
    int x, y;
} Point;

void move(Point *p, int dx, int dy) { // 간접 접근
    p->x += dx;
    p->y += dy;
}

int main() {
    Point p = { .x = 10, .y = 20 };   // 직접 접근
    printf("(%d,%d)\n", p.x, p.y);
    move(&p, 5, -3);
    printf("(%d,%d)\n", p.x, p.y);
    return 0;
}

\

3) 공용체(union)

  • 개념/필요성
    • 여러 필드가 동일한 메모리 공간을 공유합니다. 가장 큰 필드의 크기만큼만 메모리를 차지하여 메모리를 절약합니다.
    • 프로토콜/바이너리 데이터 해석, 변형 타입(Tagged Union: enum + union) 표현에 적합합니다.
    • 현재 활성 타입을 enum 등으로 함께 관리하는 것이 안전합니다.
#include <stdio.h>

typedef enum { T_INT, T_FLOAT } Type;

typedef union {
    int   i;
    float f;
} Value;

typedef struct {
    Type  type;
    Value val;
} Number;

void print_number(const Number *n) {
    if (n->type == T_INT)  printf("int: %d\n", n->val.i);
    else                   printf("float: %.2f\n", n->val.f);
}

int main() {
    Number a = { T_INT,   .val.i = 42 };
    Number b = { T_FLOAT, .val.f = 3.14f };
    print_number(&a);
    print_number(&b);
    return 0;
}

4) 열거형(enum) – “데이터들을 열거한 집합”

  • 개념/필요성
    • 의미 있는 이름의 정수 상수 집합으로 가독성 향상, switch와 궁합이 좋습니다.
    • 비트 플래그에도 활용 가능합니다.
#include <stdio.h>

typedef enum { RED = 1, GREEN = 2, BLUE = 3 } Color;

void paint(Color c) {
    switch (c) {
        case RED:   puts("RED");   break;
        case GREEN: puts("GREEN"); break;
        case BLUE:  puts("BLUE");  break;
        default:    puts("Unknown");
    }
}

/* 비트 플래그 예시 */
enum {
    PERM_READ  = 1 << 0,
    PERM_WRITE = 1 << 1,
    PERM_EXEC  = 1 << 2
};

int main() {
    paint(GREEN);
    int perm = PERM_READ | PERM_EXEC;
    if (perm & PERM_EXEC) puts("can exec");
    return 0;
}

5) 메모리 영역(코드/데이터/힙/스택)

  • 개념/필요성
    • 코드(텍스트) 영역: 기계어 코드
    • 데이터 영역: 전역/정적 변수
    • 힙: 동적 할당 영역(malloc 등)
    • 스택: 지역 변수, 호출 프레임(자동 수명)
#include <stdio.h>
#include <stdlib.h>

int g = 10;                // 데이터 영역
const char *lit = "hi";    // 보통 읽기 전용(텍스트/RODATA)

int main() {
    static int s = 20;     // 데이터 영역(정적 저장)
    int local = 30;        // 스택
    int *heap = malloc(sizeof(int)); // 힙
    *heap = 40;

    printf("code(main): %p\n", (void*)&main);
    printf("global g  : %p\n", (void*)&g);
    printf("static s  : %p\n", (void*)&s);
    printf("literal   : %p\n", (const void*)lit);
    printf("stack var : %p\n", (void*)&local);
    printf("heap ptr  : %p\n", (void*)heap);

    free(heap);
    return 0;
}

6) 동적 메모리 할당/해제

  • 개념/필요성
    • 런타임에 크기를 결정하는 데이터 관리에 필수. malloc/calloc/realloc/free 사용.
    • 체크리스트: NULL 검사, 소유권/수명 관리, 이중 해제 방지, realloc 실패 대비(임시 포인터), free 후 포인터 NULL.
#include <stdio.h>
#include <stdlib.h>

int main() {
    size_t n = 5;
    int *a = malloc(n * sizeof(int));
    if (!a) return 1;

    for (size_t i = 0; i < n; i++) a[i] = (int)i;

    size_t m = 10;
    int *tmp = realloc(a, m * sizeof(int)); // 실패 대비 임시 포인터
    if (!tmp) { free(a); return 1; }
    a = tmp;
    for (size_t i = n; i < m; i++) a[i] = (int)i;

    for (size_t i = 0; i < m; i++) printf("%d ", a[i]);
    puts("");

    free(a); a = NULL;
    return 0;
}

7) 객체지향 철학 vs 구조적 프로그래밍 vs 폭포수 모델

  • 개념/필요성
    • 구조적(절차적) 프로그래밍: 순차/분기/반복, 함수 분해로 복잡도 관리.
    • 객체지향 프로그래밍(OOP): 캡슐화, 상속, 다형성으로 모델링. C는 직접 지원 X → struct + 함수 포인터(vtable)로 유사 구현.
    • 폭포수 모델: 개발 프로세스(요구→설계→구현→테스트→배포)를 단계적으로 진행하는 방법론. 언어/패러다임과는 별개의 개념.
#include <stdio.h>
#include <stdlib.h>

/* "인터페이스" 역할의 vtable */
typedef struct ShapeVTable {
    void (*draw)(void *self);
} ShapeVTable;

/* "베이스 클래스" 역할 */
typedef struct {
    const ShapeVTable *vptr;
} Shape;

static inline void Shape_draw(Shape *s) { s->vptr->draw(s); }

/* 파생 타입: Circle */
typedef struct {
    Shape base;
    int radius;
} Circle;

void Circle_draw(void *self) {
    Circle *c = (Circle*)self;
    printf("Draw Circle r=%d\n", c->radius);
}
const ShapeVTable Circle_vt = { .draw = Circle_draw };
Circle *Circle_new(int r) {
    Circle *c = malloc(sizeof(*c));
    c->base.vptr = &Circle_vt;
    c->radius = r;
    return c;
}

/* 파생 타입: Rect */
typedef struct {
    Shape base;
    int w, h;
} Rect;

void Rect_draw(void *self) {
    Rect *r = (Rect*)self;
    printf("Draw Rect %dx%d\n", r->w, r->h);
}
const ShapeVTable Rect_vt = { .draw = Rect_draw };
Rect *Rect_new(int w, int h) {
    Rect *r = malloc(sizeof(*r));
    r->base.vptr = &Rect_vt;
    r->w = w; r->h = h;
    return r;
}

int main() {
    Shape *s1 = (Shape*)Circle_new(5);
    Shape *s2 = (Shape*)Rect_new(3, 4);

    Shape_draw(s1); // Circle_draw
    Shape_draw(s2); // Rect_draw

    free(s1);
    free(s2);
    return 0;
}

8) 추상화(Abstraction)

  • 개념/필요성
    • 불필요한 내부 구현을 숨기고, 필요한 인터페이스만 노출합니다.
    • C에서는 “불완전 타입(opaque pointer)”과 헤더(.h)/구현(.c) 분리로 달성합니다.
/* my_counter.h */
#ifndef MY_COUNTER_H
#define MY_COUNTER_H
typedef struct Counter Counter;           // 불완전 타입

Counter* counter_new(int init);
void     counter_inc(Counter *c);
int      counter_get(const Counter *c);
void     counter_free(Counter *c);

#endif
/* my_counter.c */
#include "my_counter.h"
#include <stdlib.h>

struct Counter { int value; };           // 내부는 .c에서만 공개

Counter* counter_new(int init) {
    Counter *c = malloc(sizeof(*c));
    if (c) c->value = init;
    return c;
}
void counter_inc(Counter *c) { c->value++; }
int  counter_get(const Counter *c) { return c->value; }
void counter_free(Counter *c) { free(c); }
/* main.c */
#include "my_counter.h"
#include <stdio.h>

int main() {
    Counter *c = counter_new(10);
    counter_inc(c);
    printf("%d\n", counter_get(c)); // 11
    counter_free(c);
    return 0;
}

9) 캡슐화(Encapsulation)

  • 개념/필요성
    • 내부 상태를 보호하고, 유효한 연산만 허용하여 불변성과 모듈 경계를 지킵니다.
    • C에서는 위의 opaque 타입 패턴, 모듈 스코프(static) 변수, Getter/Setter 함수 등으로 구현합니다.
/* bank.h */
#ifndef BANK_H
#define BANK_H
typedef struct BankAccount BankAccount;

BankAccount* ba_open(const char *owner, int initial);
int          ba_deposit(BankAccount *ba, int amount);
int          ba_withdraw(BankAccount *ba, int amount);
int          ba_balance(const BankAccount *ba);
void         ba_close(BankAccount *ba);

#endif
/* bank.c */
#include "bank.h"
#include <stdlib.h>
#include <string.h>

struct BankAccount {
    char *owner;
    int   balance;
};

BankAccount* ba_open(const char *owner, int initial) {
    BankAccount *ba = malloc(sizeof(*ba));
    if (!ba) return NULL;
    ba->owner = strdup(owner ? owner : "");
    ba->balance = initial;
    return ba;
}
int ba_deposit(BankAccount *ba, int amount) {
    if (amount < 0) return 0;
    ba->balance += amount;
    return 1;
}
int ba_withdraw(BankAccount *ba, int amount) {
    if (amount < 0 || ba->balance < amount) return 0;
    ba->balance -= amount;
    return 1;
}
int ba_balance(const BankAccount *ba) { return ba->balance; }
void ba_close(BankAccount *ba) {
    if (!ba) return;
    free(ba->owner);
    free(ba);
}

10) 클래스/객체/생성자/상속성(상속)/오버로딩/오버라이딩/인터페이스/람다·익명함수

  • 개념/필요성 요약
    • 클래스: 상태+행위를 묶은 추상 타입. C에는 없음 → struct + 함수로 유사 구현.
    • 객체: 클래스의 인스턴스. C에서는 struct 변수/포인터가 해당.
    • 생성자: 객체 초기화 함수(new/init). 소멸자는 free/destroy.
    • 상속: C에는 없음 → “베이스 struct를 첫 멤버로 포함 + vtable”로 유사 구현(위 Shape 예시).
    • 오버라이딩: 상속 관계에서 메서드 재정의 → vtable에 다른 함수 포인터를 채워 구현(위 Shape).
    • 오버로딩: 같은 이름 다른 시그니처. C는 미지원 → C11 _Generic으로 대체 가능.
    • 인터페이스: “해야 할 함수 집합”의 명세 → 함수 포인터 집합(struct)로 표현.
    • 람다/익명함수: 표준 C에는 없음 → “콜백 + 컨텍스트 포인터(void*)”로 캡처를 흉내.
/* 오버로딩 흉내: C11 _Generic */
#include <stdio.h>

void print_int(int x)       { printf("int:%d\n", x); }
void print_double(double x) { printf("double:%.2f\n", x); }

#define print(x) _Generic((x), \
    int:    print_int,         \
    double: print_double       \
)(x)

int main() {
    print(3);     // int 버전
    print(3.14);  // double 버전
    return 0;
}
/* 람다/익명함수 대체: 콜백 + 컨텍스트 */
#include <stdio.h>

typedef void (*Func)(int idx, void *ctx);

void for_each(int n, Func f, void *ctx) {
    for (int i = 0; i < n; i++) f(i, ctx);
}

typedef struct { int base; } AdderCtx;

void add_and_print(int i, void *ctx) {
    AdderCtx *c = (AdderCtx*)ctx;
    printf("%d\n", i + c->base);
}

int main() {
    AdderCtx ctx = { .base = 10 };
    for_each(3, add_and_print, &ctx); // 10, 11, 12
    return 0;
}

보충: “원본을 참조한다”는 말의 정확한 사용

  • 데이터 포인터 예시(진짜로 원본을 바꾸는 경우)
#include <stdio.h>

void increment(int *p) { (*p)++; }

int main() {
    int x = 10;
    increment(&x);  // 원본 x가 11로 변경
    printf("%d\n", x);
    return 0;
}
  • 함수 포인터는 “함수의 주소”를 가리킬 뿐이며, 위와 같은 원본 변경은 “데이터 포인터”로 수행됩니다. 함수 포인터는 “어떤 함수를 부를지”를 바꾸어 동작의 경로를 바꾸는 데 사용됩니다.