C 동적 할당 (malloc, free)
한눈에 보기
실행 중에 힙 메모리를 확보하고 반납하는 C의 동적 할당을 malloc, calloc, realloc, free의 동작과 할당기의 내부 구조, 메모리 누수와 댕글링 포인터 같은 대표 오류 중심으로 설명한다.
최종 업데이트 2026.09.17
C 언어의 동적 할당(Dynamic Allocation)은 프로그램 실행 중에 필요한 만큼의 메모리를 힙(Heap) 영역에서 확보하고, 다 쓰면 직접 반납하는 메모리 관리 방식이다. 컴파일 시점에 크기를 알 수 없는 데이터를 다룰 때 필수적이며, malloc과 free를 중심으로 calloc, realloc 함수가 함께 쓰인다. 해제를 프로그래머가 책임지기 때문에 메모리 누수(Memory Leak)와 댕글링 포인터(Dangling Pointer) 같은 오류를 이해하는 것이 중요하다.
왜 동적 할당이 필요한가
지역 변수와 배열은 스택(Stack)에 잡히고 크기가 컴파일 시점에 고정된다. 스택 변수는 함수가 반환되면 자동으로 사라지므로 함수 밖으로 살아남는 데이터를 만들 수 없고, 스택 크기 자체도 보통 수 MB 수준으로 제한된다. 반면 사용자 입력 개수만큼 배열을 만들거나, 연결 리스트(Linked List)와 트리처럼 실행 중에 크기가 변하는 자료구조를 구현하려면 실행 시점에 크기를 정할 수 있어야 한다. 힙은 프로그래머가 명시적으로 해제하기 전까지 유지되고 스택보다 훨씬 큰 공간을 쓸 수 있어 이런 요구를 충족한다.
malloc, calloc, realloc, free
malloc(size)은 요청한 바이트 수만큼 힙에서 공간을 확보하고 시작 주소를 void 포인터로 반환한다. 할당된 공간은 초기화되지 않아 쓰레기 값이 들어 있다. calloc(n, size)은 n개 곱하기 size 바이트를 할당하면서 전체를 0으로 초기화한다는 점이 다르다. realloc(ptr, newSize)은 기존 블록의 크기를 조정하는데, 제자리에서 늘릴 수 없으면 새 블록을 할당해 기존 내용을 복사하고 옛 블록을 해제하므로 반환된 주소가 바뀔 수 있다. free(ptr)는 할당받은 블록을 반납한다.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int n = 5;
int *arr = (int *)malloc(sizeof(int) * n);
if (arr == NULL) { /* 할당 실패 확인은 필수다 */
return 1;
}
for (int i = 0; i < n; i++) {
arr[i] = i * 10;
}
int *tmp = (int *)realloc(arr, sizeof(int) * n * 2);
if (tmp == NULL) { /* 실패 시 arr는 여전히 유효하다 */
free(arr);
return 1;
}
arr = tmp;
free(arr);
arr = NULL; /* 댕글링 포인터 방지 습관 */
return 0;
}
할당에 실패하면 NULL이 반환되므로 반드시 검사해야 한다. 특히 realloc의 반환값을 원본 포인터에 바로 대입하면, 실패했을 때 원본 주소를 잃어버려 해제할 방법이 없어지므로 위 예시처럼 임시 포인터로 받는 것이 안전하다.
메모리 관점에서 본 동작
malloc이 반환하는 블록 앞에는 할당기(Allocator)가 관리하는 헤더가 붙어 블록 크기 같은 메타데이터가 저장된다. free(ptr)에 크기를 넘기지 않아도 해제가 가능한 이유가 이 헤더 덕분이다. 할당기는 해제된 블록들을 가용 리스트(Free List) 같은 구조로 관리하다가 다음 요청에 재사용하며, 힙 공간이 부족하면 brk나 mmap 같은 시스템 호출(System Call)로 운영체제에서 추가 공간을 받아온다. 시스템 호출은 비용이 크기 때문에 할당기가 중간에서 공간을 재활용하는 것이다.
할당과 해제가 반복되면 힙 곳곳에 작은 빈 블록이 흩어지는 단편화(Fragmentation)가 발생한다. 전체 가용 공간은 충분한데 연속된 큰 블록이 없어 할당에 실패할 수 있으며, 장시간 실행되는 서버 프로그램에서 특히 문제가 된다.
대표적인 오류 유형
| 오류 | 원인 | 결과 |
|---|---|---|
| 메모리 누수 | 할당 후 free를 호출하지 않고 포인터를 잃어버림 | 사용 가능한 메모리가 점점 줄어듦 |
| 댕글링 포인터 | free 이후에도 해제된 주소를 참조 | 정의되지 않은 동작, 데이터 오염 |
| 이중 해제(Double Free) | 같은 포인터를 두 번 free | 힙 메타데이터 손상, 보안 취약점 |
| 버퍼 오버플로(Buffer Overflow) | 할당 크기를 넘어 쓰기 | 인접 블록과 헤더 파괴 |
이런 오류는 발생 지점과 증상이 멀리 떨어져 나타나 디버깅이 어렵다. free 직후 포인터에 NULL을 대입하는 습관, 할당과 해제의 책임 주체를 명확히 하는 설계, 그리고 Valgrind나 AddressSanitizer 같은 도구를 이용한 검증이 실무적인 대응책이다. 참고로 C++에서는 new와 delete를 쓰지만, 모던 C++(Modern C++)에서는 std::unique_ptr 같은 스마트 포인터(Smart Pointer)로 해제를 자동화해 이 문제들을 원천적으로 줄인다.
스택 할당과의 비교
| 구분 | 스택 | 힙 |
|---|---|---|
| 크기 결정 시점 | 컴파일 시점 | 실행 시점 |
| 해제 방식 | 함수 반환 시 자동 | free로 수동 |
| 속도 | 포인터 이동만으로 매우 빠름 | 가용 블록 탐색이 필요해 상대적으로 느림 |
| 크기 제한 | 수 MB 수준으로 작음 | 가용 메모리 한도까지 큼 |
| 수명 | 함수 범위에 종속 | 해제 전까지 유지 |
면접에서 이렇게 물어본다
- malloc과 calloc의 차이는 무엇이며, 각각 언제 쓰는 것이 적절한가?
- free에 크기를 넘기지 않는데 어떻게 할당된 크기만큼 정확히 해제될 수 있는가?
- 메모리 누수와 댕글링 포인터는 각각 무엇이고 어떻게 예방하는가?
- realloc 반환값을 원본 포인터에 바로 대입하면 어떤 문제가 생길 수 있는가?
- 스택 할당과 힙 할당의 차이를 속도, 수명, 크기 관점에서 비교해 보라.
C / C++에서 같이 읽기
- C 언어 컴파일 과정 (Compile Process)C 소스 코드가 전처리, 컴파일, 어셈블, 링킹 네 단계를 거쳐 실행 파일이 되는 과정을 단계별 산출물, 목적 파일의 섹션 구조, 정적 링킹과 동적 링킹의 차이와 함께 정리한다.
- 포인터 (Pointer)다른 변수의 메모리 주소를 저장하는 포인터의 개념과 역참조, 포인터 산술, 배열과의 관계, 이중 포인터와 함수 포인터, 그리고 널 포인터와 댕글링 포인터 같은 오류 상황까지 정리한다.
- 구조체 메모리 크기와 정렬, 패딩 (Struct Alignment)구조체 크기가 멤버 합보다 커지는 이유인 메모리 정렬과 패딩의 규칙을 오프셋 계산 예시로 설명하고, 멤버 순서에 따른 크기 차이와 #pragma pack 사용 시 주의점을 다룬다.
- C++ 입출력 성능 최적화 (I/O Optimization)cin과 cout이 기본 설정에서 느린 이유를 stdio 동기화와 버퍼 플러시로 설명하고, sync_with_stdio(false), cin.tie(NULL), endl 대신 개행 문자를 쓰는 세 가지 최적화와 주의점을 정리한다.
이 개념, 내 코드에서는 어떻게 물어볼까요
데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.
공고 넣고 시작하기