C에서 스택과 힙 메모리를 안전하게 관리하는 방법

C 언어의 스택과 힙 수명 차이, 구조체 접근 연산자, 동적 할당 오류 처리와 BFS 메모리 관리 패턴을 정리한다.

2026-08-14 · 최초 발행 2024-04-29

수명과 소유권이 C 코드의 경계를 만든다

C는 메모리를 직접 제어할 수 있는 대신, 객체가 언제까지 살아 있어야 하는지와 누가 해제할지를 개발자가 결정해야 한다. 함수 안에서만 쓰는 객체인지, 함수 범위를 벗어나서도 유지되어야 하는 객체인지에 따라 스택과 힙의 선택이 달라진다.

스택 메모리는 함수 호출에 따라 자동으로 확보되고, 스코프가 끝나면 정리되는 저장 영역이다. 지역 변수와 매개변수가 여기에 놓이며 수명이 비교적 예측 가능하다. 반면 힙은 malloc, calloc, realloc, free로 직접 다루는 동적 저장 영역이다. 필요한 크기와 수명을 유연하게 정할 수 있지만, 해제를 빠뜨리면 메모리 누수가 남는다.

구조체 접근 방식도 객체의 형태를 따라야 한다. 값으로 가진 구조체에는 .을 사용하고, 구조체 포인터에는 ->를 사용한다. 따라서 Node.dataNodePtr->data는 각각 값과 포인터에 대응한다. 같은 원리로 값 구조체의 필드를 증가시킬 때는 Stack.Top++, 포인터로 가리키는 구조체의 필드에는 Stack->Top++를 사용한다.

스택과 힙을 선택할 때 확인할 조건

스택 변수는 함수가 끝날 때 자동 해제되므로 단기 객체에 적합하다. 할당과 해제는 매우 빠르고 수명도 스코프에 맞춰 일관되게 유지된다. 다만 스택 크기 한계가 있어 대용량 배열에는 주의해야 한다.

힙 객체는 범위를 벗어나서도 생존할 수 있고 크기와 수명을 유연하게 다룰 수 있다. 그 대신 소유권과 해제 책임을 코드와 API 설계에 명확히 남겨야 한다. 소유권을 전달하지 않는 포인터에는 const를 고려하고, 소유권이 이전되는 경우에는 그 사실을 문서화할 필요가 있다.

구분 성능 확장성 일관성 안정성 운영 편의
스택 매우 빠름(상수시간 할당/해제) 제한적(스택 크기 한계) 스코프 기반 수명 일관 오버플로우 위험(대용량 배열 금지) 자동 해제로 편의성 높음
상대적 느림(할당기 오버헤드) 매우 유연 수명·소유권 명확화 필요 누수·단편화 위험 free 필요, 도구로 검증 용이

할당 실패와 해제 경로를 함께 설계한다

동적 할당은 필요한 용량, 정렬, 수명 요구사항을 먼저 정한 뒤 성공 여부를 확인하고 초기화와 사용으로 이어져야 한다. 오류가 발생한 경로에서도 이미 확보한 리소스를 해제해야 한다.

malloc 반환값의 NULL 검사는 필수다. 반환형으로 오류를 표현하고 리소스 정리를 단일 출구 패턴으로 모으면, 누수와 이중 해제, use-after-free를 줄이는 데 도움이 된다.

아니오아니오아니오입력: 객체 필요/시작 정점 ID객체 수명 범위가 함수 스코프이내인가스택 변수 선언: struct S s;. 연산자로 접근: s.top++,s.data스코프 종료 자동 해제 할당: S* p = malloc(sizeof*p)할당 성공?오류 반환 로그: malloc실패- 연산자로 접근: p- top++, p-data정상 종료 경로에서 free(p)시작 정점 탐색:FindVertex(id)NULL?오류 메시지 즉시 반환BFS 수행:초기화→방문표시→인접 탐색

힙에 만든 스택은 해제 순서까지 소유한다

다음 예제는 구조체와 내부 버퍼를 모두 힙에 두는 방식이다. Stack이 힙 객체이므로 멤버에는 ->로 접근한다. 해제는 내부 버퍼, 외부 구조체, 널 포인터화 순서로 진행한다.

환경은 C11, gcc/clang, Linux/Windows이며 -Wall -Wextra -O2를 권장한다. 실패 시 errno 확인이 가능하다.

// stack_heap.c
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

typedef struct {
    int    *data;
    size_t  capacity;
    size_t  top;  // next write index
} Stack;

bool StackInit(Stack **ps, size_t cap) {
    Stack *s = (Stack *)malloc(sizeof *s);
    if (!s) return false;
    s->data = (int *)malloc(cap * sizeof *s->data);
    if (!s->data) { free(s); return false; }
    s->capacity = cap;
    s->top = 0;
    *ps = s;
    return true;
}

void StackDestroy(Stack **ps) {
    if (!ps || !*ps) return;
    free((*ps)->data);
    free(*ps);
    *ps = NULL;
}

bool StackPush(Stack *s, int v) {
    if (s->top >= s->capacity) return false;
    s->data[s->top++] = v;         // Stack->Top , Stack->Top++ 패턴
    return true;
}

bool StackPop(Stack *s, int *out) {
    if (s->top == 0) return false;
    *out = s->data[--s->top];
    return true;
}

int main(void) {
    Stack *s = NULL;
    if (!StackInit(&s, 4)) { fprintf(stderr, "malloc 실패\n"); return 1; }

    StackPush(s, 10);
    StackPush(s, 20);

    int x;
    if (StackPop(s, &x)) printf("pop=%d\n", x);

    StackDestroy(&s); // 힙 메모리 해제 필수
    return 0;
}

자동 수명으로 충분한 스택 구조체

구조체 자체를 자동 변수로 선언하면 별도의 free가 필요 없다. st는 값 구조체이므로 멤버 접근에 .을 쓴다. 스코프를 벗어나면 자동으로 해제된다.

// stack_auto.c
#include <stdio.h>
#include <stdbool.h>

#define CAP 4

typedef struct {
    int    data[CAP];
    size_t capacity;
    size_t top;
} Stack;

void StackInit(Stack *s) { s->capacity = CAP; s->top = 0; }
bool StackPush(Stack *s, int v) { if (s->top >= s->capacity) return false; s->data[s->top++] = v; return true; }
bool StackPop(Stack *s, int *out) { if (s->top == 0) return false; *out = s->data[--s->top]; return true; }

int main(void) {
    Stack st;                  // 스택 메모리 영역 사용 (자동 수명)
    StackInit(&st);
    StackPush(&st, 1);         // Stack.Top++ 패턴
    StackPush(&st, 2);

    for (size_t i = 0; i < st.top; ++i) {
        printf("%d ", st.data[i]);  // Node.data 유사 접근
    }
    printf("\n");
    return 0; // 스코프 종료 시 자동 해제
}

BFS에서 입력 검증과 리소스 정리를 연결하는 방식

BFS는 입력 키로 정점을 찾고 NULL 여부를 검증한 뒤 큐 기반 탐색을 수행해야 한다. 방문 배열과 큐는 힙에 할당되므로, 할당 실패를 포함한 모든 종료 경로에서 정리해야 한다. 방문 배열은 O(V) 할당과 해제를 보장한다.

다음 예제는 FindVertex로 외부 키인 id와 내부 인덱스를 분리한다. 시작 정점이 없으면 즉시 반환하고, 큐와 방문 배열의 할당 실패도 별도로 처리한다.

// bfs.c
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

typedef struct {
    int     id;        // 외부에 노출되는 키
    size_t  degree;
    size_t *adj;       // 인접 정점의 인덱스 배열
} Vertex;

typedef struct {
    size_t   n;
    Vertex  *v;
} Graph;

ssize_t FindVertex(const Graph *g, int id) {
    if (!g) return -1;
    for (size_t i = 0; i < g->n; ++i) if (g->v[i].id == id) return (ssize_t)i;
    return -1;
}

bool BFS(const Graph *g, int startId) {
    if (!g || g->n == 0) { fprintf(stderr, "그래프 없음\n"); return false; }

    ssize_t sidx = FindVertex(g, startId);
    if (sidx < 0) { printf("No Data\n"); return false; } // 사용 예: if(V==NULL){printf(No Data);}

    bool   *vis = (bool *)calloc(g->n, sizeof *vis);
    size_t *q   = (size_t *)malloc(g->n * sizeof *q);
    if (!vis || !q) { free(vis); free(q); fprintf(stderr, "메모리 부족\n"); return false; }

    size_t head = 0, tail = 0;
    q[tail++] = (size_t)sidx;
    vis[sidx] = true;

    while (head < tail) {
        size_t u = q[head++];
        printf("visit id=%d\n", g->v[u].id);

        for (size_t i = 0; i < g->v[u].degree; ++i) {
            size_t w = g->v[u].adj[i];
            if (!vis[w]) {
                vis[w] = true;
                q[tail++] = w;
            }
        }
    }

    free(vis);
    free(q);
    return true;
}

// 샘플 그래프 빌드/해제
void FreeGraph(Graph *g) {
    if (!g || !g->v) return;
    for (size_t i = 0; i < g->n; ++i) free(g->v[i].adj);
    free(g->v);
    g->v = NULL; g->n = 0;
}

int main(void) {
    Graph g = {0};
    g.n = 3;
    g.v = (Vertex *)calloc(g.n, sizeof *g.v);
    if (!g.v) return 1;
    g.v[0].id = 10; g.v[1].id = 20; g.v[2].id = 30;

    g.v[0].degree = 2; g.v[0].adj = (size_t *)malloc(2 * sizeof(size_t)); g.v[0].adj[0]=1; g.v[0].adj[1]=2;
    g.v[1].degree = 1; g.v[1].adj = (size_t *)malloc(1 * sizeof(size_t)); g.v[1].adj[0]=2;
    g.v[2].degree = 0; g.v[2].adj = NULL;

    BFS(&g, 10);      // 정상 탐색
    BFS(&g, 99);      // No Data

    FreeGraph(&g);
    return 0;
}

스택에는 . 접근과 자동 수명 관리가, 힙 객체에는 -> 접근과 명시적 해제가 따른다. 동적 할당을 사용한다면 실패 처리와 해제 경로를 함께 작성해야 한다. 단기 객체에는 스택을 사용하고 힙 사용 범위를 필요한 곳으로 제한하면 allocator 호출 수를 줄일 수 있다.

일관된 free 규칙과 해제 함수를 제공하면 유지보수성이 높아진다. 정적 분석기와 Valgrind를 사용할 때는 누수 0건을 목표로 삼을 수 있다.

C 언어메모리 관리스택BFS