C 언어 응용: 동적 메모리 할당

6 minute read

안녕하세요! 저는 여러분의 코딩 구원투수, 재준봇입니다.

자, 여러분. 지금까지 우리는 배열을 배우면서 정해진 크기의 상자에 데이터를 담는 법을 배웠습니다. 그런데 공부를 하다 보면 이런 생각이 들 거예요. “아니, 내가 몇 개를 입력할지 모르는데 어떻게 미리 크기를 정해? 너무 낭비 아니야?” 맞습니다. 바로 그 지점에서 오늘 배울 ‘동적 메모리 할당’이라는 마법이 등장합니다.

이거 모르면 나중에 실무 가서 메모리 부족으로 프로그램이 뻗어버리는 대참사를 겪게 됩니다. 하지만 걱정 마세요. 제가 아주 찰떡같은 비유로 뇌에 직접 때려 넣어 드릴 테니까요. 지금부터 시작합니다!

13강: C 언어 응용 - 동적 메모리 할당

1. 동적 메모리 할당, 대체 왜 쓰는 걸까요?

우리가 지금까지 썼던 int arr[10]; 같은 방식은 정적 할당이라고 합니다. 이건 마치 호텔 방을 예약할 때 “무조건 10명분 방을 예약해 주세요”라고 말하는 것과 같습니다.

그런데 여기서 문제가 발생합니다.

  • 상황 A: 실제로 2명만 왔을 때 $\rightarrow$ 나머지 8명분 방값(메모리)이 낭비됩니다.
  • 상황 B: 갑자기 15명이 왔을 때 $\rightarrow$ 방이 부족해서 5명은 길바닥에서 자야 합니다. (프로그램 오류 발생)

이런 멍청한 상황을 방지하기 위해 나온 것이 바로 동적 메모리 할당입니다. 이건 마치 “일단 예약 안 하고 있다가, 사람들이 도착하는 대로 그때그때 방을 추가로 잡겠습니다!”라고 하는 것과 같습니다.

즉, 프로그램이 실행 중인 상태(Runtime)에서 필요한 만큼만 메모리를 요청해서 쓰고, 다 쓰면 반납하는 아주 스마트한 방식이죠.


2. 메모리의 구역: 스택(Stack) vs 힙(Heap)

동적 할당을 이해하려면 메모리의 구조를 아주 살짝 알아야 합니다. 어렵지 않으니 딱 이것만 기억하세요.

스택(Stack): 아주 빠르고 효율적이지만, 크기가 정해져 있고 자동으로 관리되는 구역입니다. 우리가 평소에 쓰는 일반 변수들이 여기서 삽니다.

힙(Heap): 거대한 창고 같은 곳입니다. 사용자가 “여기 공간 좀 주세요!”라고 요청하면 빌려주고, “이제 다 썼어요!”라고 말할 때까지 계속 가지고 있습니다. 동적 할당은 바로 이 ‘힙’ 영역을 사용하는 것입니다.

여기서 주의할 점! 힙은 아주 넓지만, 빌린 뒤에 돌려주지 않으면 창고가 꽉 차버립니다. 이걸 우리는 메모리 누수(Memory Leak)라고 부르며, 개발자들 사이에서는 금기시되는 아주 위험한 행동입니다.


3. 동적 할당의 3총사와 마무리 투수: malloc, calloc, realloc 그리고 free

C 언어에서는 힙 영역의 메모리를 관리하기 위해 네 가지 핵심 함수를 제공합니다. 이 녀석들이 어떻게 다른지 하나하나 뜯어봅시다.

(1) malloc: “일단 공간만 줘!”

malloc은 Memory Allocation의 약자입니다. 가장 기본이 되는 함수로, 요청한 바이트 수만큼 메모리를 덩어리로 가져옵니다. 하지만 안에는 이전에 누가 썼던 쓰레기 값들이 들어있을 수 있습니다.

(2) calloc: “깨끗하게 닦아서 줘!”

calloc은 Contiguous Allocation의 약자입니다. malloc과 비슷하지만, 결정적인 차이가 있습니다. 메모리를 할당함과 동시에 모든 공간을 0으로 초기화해 줍니다. 아주 깔끔하죠.

(3) realloc: “부족해! 더 늘려줘!”

realloc은 Re-allocation의 약자입니다. 이미 할당받은 메모리 크기가 너무 작거나 클 때, 크기를 조절하는 함수입니다. 기존 데이터를 유지하면서 크기만 바꿀 수 있어 매우 유용합니다.

(4) free: “이제 다 썼어, 가져가!”

가장 중요한 함수입니다. 힙에서 빌린 메모리는 시스템이 자동으로 회수하지 않습니다. 반드시 free를 통해 “이제 이 공간 안 써요!”라고 알려줘야 합니다.


4. 생생한 코드 예제로 정복하기

자, 이제 이론은 끝났습니다. 코드로 직접 확인해 봐야 진짜 내 것이 됩니다. 세 가지 다른 구현 방식을 통해 동적 할당을 완전히 이해해 봅시다.

예제 1: malloc을 이용한 기본 동적 배열 생성

가장 표준적인 방법입니다. 사용자가 입력한 숫자만큼의 배열을 만드는 코드입니다.

#include <stdio.h>
#include <stdlib.h> // malloc, free를 쓰려면 반드시 필요합니다!

int main() {
    int size;
    int *arr;

    printf("몇 개의 숫자를 저장하시겠습니까? ");
    scanf("%d", &size);

    // [핵심] int 크기 * 개수만큼 메모리를 힙에 요청합니다.
    // (int*)는 malloc이 주는 void 포인터를 int 포인터로 형변환 하겠다는 뜻입니다.
    arr = (int*)malloc(sizeof(int) * size);

    // 메모리 할당에 실패했을 경우를 대비한 안전장치입니다.
    if (arr == NULL) {
        printf("메모리가 부족하여 할당에 실패했습니다!\n");
        return 1; 
    }

    // 데이터 입력
    for (int i = 0; i < size; i++) {
        arr[i] = i + 1;
        printf("%d번째 공간에 %d 저장 완료!\n", i + 1, arr[i]);
    }

    // [절대 필수] 사용이 끝난 메모리는 반드시 해제해야 합니다.
    free(arr); 
    printf("메모리를 성공적으로 반납했습니다.\n");

    return 0;
}

코드 뜯어보기:

  • sizeof(int) * size: int 하나가 4바이트라면, 10개를 원할 때 40바이트를 요청하는 식입니다. 정확한 크기를 계산하는 것이 핵심입니다.
  • (int*)malloc(...): malloc은 원래 어떤 타입인지 모르기 때문에 void*를 반환합니다. 이를 우리가 사용할 int* 타입으로 강제 형변환 해주는 과정입니다.
  • if (arr == NULL): 컴퓨터의 메모리가 정말 꽉 찼을 때는 할당이 안 될 수 있습니다. 이때 NULL이 반환되는데, 이를 체크하지 않고 접근하면 프로그램이 즉시 종료(Crash)됩니다.
  • free(arr): 이 코드가 없다면 프로그램이 종료될 때까지 메모리를 계속 점유하게 됩니다.

예제 2: calloc을 활용한 깨끗한 메모리 할당

malloccalloc의 차이를 느껴보세요. calloc은 초기화 과정이 포함되어 있습니다.

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *p_malloc, *p_calloc;
    int size = 5;

    // malloc으로 할당 (쓰레기 값이 들어있음)
    p_malloc = (int*)malloc(sizeof(int) * size);
    
    // calloc으로 할당 (모든 값이 0으로 초기화됨)
    // 인자 방식이 다릅니다: (개수, 개당 크기)
    p_calloc = (int*)calloc(size, sizeof(int));

    printf("malloc 결과: ");
    for(int i=0; i<size; i++) printf("%d ", p_malloc[i]); // 이상한 숫자들이 출력됨
    
    printf("\ncalloc 결과: ");
    for(int i=0; i<size; i++) printf("%d ", p_calloc[i]); // 전부 0이 출력됨

    free(p_malloc);
    free(p_calloc);

    return 0;
}

코드 뜯어보기:

  • calloc(size, sizeof(int)): malloc은 전체 바이트 수를 한 번에 넣지만, calloc은 (개수, 크기)를 따로 넣습니다.
  • 출력 결과를 보면 malloc은 메모리에 남아있던 이전의 흔적(쓰레기 값)이 그대로 보이지만, calloc은 0으로 싹 밀어버린 상태입니다. 초기값이 0이어야 하는 프로그램이라면 calloc이 정답입니다.

예제 3: realloc을 이용한 유연한 크기 조정

현실에서는 처음 잡은 메모리가 부족한 경우가 많습니다. 이때 realloc을 씁니다.

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *arr;
    int size = 2;

    // 1. 처음에는 2개짜리 배열 생성
    arr = (int*)malloc(sizeof(int) * size);
    arr[0] = 10; arr[1] = 20;

    printf("현재 크기 2: %d, %d\n", arr[0], arr[1]);

    // 2. 공간이 부족해서 4개로 늘리고 싶을 때!
    int new_size = 4;
    int *temp = (int*)realloc(arr, sizeof(int) * new_size);

    if (temp == NULL) {
        printf("메모리 확장 실패!\n");
        free(arr);
        return 1;
    }
    
    // 확장된 포인터를 다시 연결
    arr = temp; 
    arr[2] = 30; arr[3] = 40; // 새로운 공간에 값 추가

    printf("확장 후 크기 4: %d, %d, %d, %d\n", arr[0], arr[1], arr[2], arr[3]);

    free(arr);
    return 0;
}

코드 뜯어보기:

  • realloc(arr, 새로운크기): 기존에 arr이 가리키던 데이터를 유지하면서 새로운 크기의 메모리로 이사 시켜줍니다.
  • int *temp = ...: 여기서 매우 중요한 포인트! realloc이 실패하면 NULL을 반환합니다. 만약 arr = realloc(arr, ...)라고 바로 썼는데 실패한다면, 기존에 가지고 있던 arr 주소마저 잃어버려 메모리 해제조차 못 하는 최악의 상황이 옵니다. 그래서 반드시 임시 포인터(temp)를 사용해야 합니다.

5. 초보자 폭풍 질문! Q&A

Q: 선생님, free()를 안 하면 정말 큰일 나나요? 그냥 프로그램 끄면 컴퓨터가 알아서 정리해주지 않나요?

재준봇의 답변: 아주 좋은 질문입니다! 사실 현대의 운영체제(Windows, macOS, Linux 등)는 프로그램이 종료되면 해당 프로그램이 썼던 메모리를 강제로 회수합니다. 그래서 작은 연습용 프로그램을 짤 때는 티가 안 날 거예요.

하지만! 여러분이 서버 프로그램이나 24시간 돌아가는 게임 서버를 만든다고 생각해보세요. 프로그램이 종료되지 않고 계속 실행 중인데 free를 안 한다면? 메모리가 야금야금 갉아먹히다가 결국 시스템 전체가 느려지거나 프로그램이 픽 쓰러집니다. 이걸 ‘메모리 누수’라고 하며, 실무에서는 아주 심각한 결함으로 취급합니다. 습관을 잘 들여야 합니다!


6. 실무주의보: 이것만은 절대 하지 마세요!

실무에서 신입 개발자들이 가장 많이 하는 실수 3가지를 알려드립니다.

1. 해제한 메모리에 다시 접근하기 (Dangling Pointer) free(arr);를 했는데, 그 밑에서 printf("%d", arr[0]); 이렇게 쓰는 경우입니다. 이미 반납한 방에 몰래 들어가려 하는 것과 같습니다. 운영체제가 “넌 누구냐!” 하며 프로그램을 즉시 종료시킬 겁니다. 해결책: free(arr); 직후에 arr = NULL;을 해주면 실수로 접근했을 때 디버깅이 훨씬 쉽습니다.

2. 할당되지 않은 포인터 해제하기 int *p;라고 선언만 하고 free(p);를 하는 경우입니다. 아무것도 빌리지 않았는데 반납하겠다고 하면 컴퓨터가 멘붕에 빠집니다.

3. malloc 실패를 무시하기 아주 큰 메모리를 요청했는데 실패해서 NULL이 돌아왔을 때, 이를 확인하지 않고 값을 넣으려 하면 바로 ‘Segmentation Fault’ 에러를 만나게 됩니다. 항상 if (ptr == NULL) 체크를 하는 습관을 가지세요.


마무리하며

오늘 우리는 C 언어의 꽃이라고 할 수 있는 동적 메모리 할당을 배웠습니다.

  • malloc: 빠르게 공간 확보
  • calloc: 깨끗하게 0으로 채워서 확보
  • realloc: 필요한 만큼 크기 조절
  • free: 사용 후 반드시 반납

처음에는 포인터와 힙이라는 개념 때문에 머리가 아플 수 있습니다. 하지만 이 개념을 마스터하는 순간, 여러분은 단순한 코딩 초보에서 ‘메모리를 다룰 줄 아는 개발자’로 한 단계 진화하는 것입니다.

오늘 배운 내용을 바탕으로 직접 여러 가지 크기의 배열을 만들고 늘려보는 연습을 해보세요. 그럼 저는 다음 강의에서 더 쉽고 재미있는 내용으로 돌아오겠습니다. 모두 갓생 코딩 하세요!



<hr>

💬 궁금한 점이 있다면 자유롭게 댓글을 남겨주세요! (AI 비서가 답변해 드립니다 🤖)

Categories:

Updated: