12편에서 stdio.h를 사용해 파일과 입력을 다뤘다면, 이제는 "필요한 만큼의 메모리를 직접 빌렸다가 돌려주는" 동적 메모리 할당 차례입니다. 지금까지는 배열 길이를 컴파일 시점에 정했지만, 사용자 입력이나 파일 길이는 실행 전에 알 수 없습니다. malloc과 free는 이런 상황에서 프로그램이 스스로 공간을 요청하고 해제하도록 도와줍니다.
이번 글에서 새로 나오는 용어
- 힙 (Heap): 실행 중 필요한 만큼 메모리를 요청해 쓰는 영역
- 동적 메모리 할당: 프로그램 실행 중
malloc같은 함수를 통해 메모리를 확보하는 과정 - size_t: "얼마나 큰가"를 표현할 때 쓰는 부호 없는 정수 타입
- 메모리 누수 (Memory Leak): 할당한 메모리를
free하지 않아 더 이상 접근할 길을 잃은 상태
핵심 개념
학습 메모
- 소요 시간: 60~90분
- 준비물: 배열, 포인터 기초,
stdio.h입출력 경험- 학습 목표:
malloc계열 함수를 사용해 공간을 확보하고,free로 수명을 명확히 끝내기
정적 배열은 길이를 바꾸기 쉽지 않기 때문에 입력 크기를 모르거나 점점 늘어나는 데이터를 다루기 어렵습니다. 예를 들어 사용자가 몇 글자를 입력할지, 파일에 몇 개의 점수가 들어 있을지 실행 전에는 모를 수 있습니다. 동적 메모리는 바로 이런 "실행 중에 크기가 정해지는 데이터"를 다룰 때 필요합니다. 동적 메모리는 아래 단계를 항상 함께 생각해야 합니다.
- 요청: 필요한 바이트 수를 계산해
malloc/calloc/realloc으로 확보합니다. - 확인: 반환값이
NULL인지 확인해 실제로 할당이 성공했는지 검사합니다. - 사용: 포인터를 배열처럼 쓰거나 다른 포인터에게 넘깁니다.
- 해제: 더 이상 필요 없을 때
free로 돌려주고, 포인터를NULL로 초기화하면 안전합니다.
이번 글에서는 아래 시나리오를 중심으로 살펴봅니다.
- 정적 배열과 동적 메모리 차이 이해하기
malloc,calloc,realloc의 공통점과 차이점free를 꼭 호출해야 하는 이유와 누수를 찾는 감각- 실패 가능성을 대비한
NULL체크 패턴 - 입력 길이에 따라 버퍼를 늘리는 간단한 예제
코드로 따라하기
스택 배열과 힙 메모리 비교
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int fixed[3] = {1, 2, 3};
int *dynamic = malloc(sizeof(int) * 3);
if (dynamic == NULL) {
return 1;
}
for (int i = 0; i < 3; i++) {
dynamic[i] = (i + 1) * 10;
}
printf("fixed[1] = %d\n", fixed[1]);
printf("dynamic[1] = %d\n", dynamic[1]);
free(dynamic);
return 0;
}
fixed는 컴파일 시점에 크기가 정해지고 스택에 배치됩니다. dynamic은 실행 중에 힙에서 공간을 빌리고, free(dynamic);으로 직접 돌려줍니다.
malloc으로 필요한 만큼 요청하기
#include <stdio.h>
#include <stdlib.h>
int main(void) {
size_t count = 5;
int *scores = malloc(sizeof(int) * count);
if (scores == NULL) {
printf("메모리를 확보하지 못했습니다.\n");
return 1;
}
for (size_t i = 0; i < count; i++) {
scores[i] = (int)(i * 10);
}
for (size_t i = 0; i < count; i++) {
printf("scores[%zu] = %d\n", i, scores[i]);
}
free(scores);
scores = NULL;
return 0;
}
sizeof(int) * count처럼 "필요한 요소 수 × 한 요소 크기"로 계산합니다.size_t는 크기를 표현할 때 사용하는 표준 타입이라 음수가 없고, 플랫폼마다 자동으로 적절한 크기를 가집니다.free이후 포인터를NULL로 바꿔 두면 잘못된 재사용을 빨리 발견할 수 있습니다.
calloc으로 0으로 초기화된 공간 얻기
#include <stdio.h>
#include <stdlib.h>
int main(void) {
size_t count = 4;
int *buffer = calloc(count, sizeof(int));
if (buffer == NULL) {
return 1;
}
for (size_t i = 0; i < count; i++) {
printf("buffer[%zu] = %d\n", i, buffer[i]);
}
free(buffer);
return 0;
}
calloc은 요소 수와 요소 크기를 따로 받으며, 모든 바이트를 0으로 채워 줍니다. 배열 값을 일일이 0으로 채우는 수고를 덜 수 있지만, 모든 자료형에서 이것이 언제나 "논리적으로 원하는 초기값"과 완전히 같다고 단정하면 안 됩니다. 입문 단계에서는 정수 배열을 0으로 준비하는 예제로 먼저 이해하면 충분합니다.
realloc으로 버퍼 크기 늘리기
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
size_t capacity = 4;
size_t length = 0;
char *line = malloc(capacity);
if (line == NULL) {
return 1;
}
const char *source = "C-memory";
for (size_t i = 0; source[i] != '\0'; i++) {
if (length + 1 >= capacity) {
size_t new_capacity = capacity * 2;
char *temp = realloc(line, new_capacity);
if (temp == NULL) {
free(line);
return 1;
}
line = temp;
capacity = new_capacity;
}
line[length++] = source[i];
}
line[length] = '\0';
printf("line = %s (capacity = %zu)\n", line, capacity);
free(line);
return 0;
}
realloc은 기존 포인터를 새로운 크기로 늘이거나 줄입니다.- 반환값이 다를 수 있으므로, 임시 포인터(
temp)로 받아서 실패했을 때 원본을 잃지 않게 합니다. - 성공하면 새 포인터를 기준으로만 계속 사용하고, 실패했을 때만 원본 포인터가 그대로 남아 있다고 생각하면 됩니다.
- 새로운 공간이 이전 주소와 다를 수 있어 복사 후 오래된 포인터를 사용하면 안 됩니다.
실패 가능성을 항상 염두에 두기
힙 공간은 무한하지 않으므로, malloc이 실패하면 NULL을 반환합니다. 따라서 아래 패턴을 기본으로 익혀 둡니다.
int *data = malloc(sizeof(int) * n);
if (data == NULL) {
// 로그를 남기거나 사용자에게 알리고, 안전하게 종료
}
또한 free는 NULL 포인터를 인자로 받을 때 아무 일도 하지 않으므로, "이미 해제했는지 확신이 없다면 먼저 NULL인지 확인"하는 습관을 들이면 좋습니다.
누수를 찾아내는 생활 습관
간단한 규칙을 코드 리뷰 리스트로 만들어 두면 누수를 줄일 수 있습니다.
malloc을 호출한 함수가free까지 책임질 것인지, 아니면 호출자에게 포인터를 반환할 것인지 역할을 명확히 구분합니다.return문이 여러 개인 함수에서는 "모든 경로에서free가 호출되는가"를 체크합니다.- 동적 배열을 구조체에 넣을 때는 구조체를 해제하는 함수에
free호출을 포함시킵니다.
왜 중요한가
- 입력 길이, 파일 크기, 네트워크 데이터 등 실행 중 결정되는 정보를 다루려면 동적 메모리가 필수입니다.
malloc과free를 이해하면 이후 포인터 산술, 구조체, 사용자 정의 컨테이너까지 확장할 수 있습니다.- 메모리 누수를 스스로 추적하는 습관은 시스템 프로그래밍뿐 아니라 다른 언어의 리소스 관리(파일, 소켓 등)에도 그대로 응용됩니다.
실습
- 따라 하기:
malloc/calloc/realloc예제를 각각 실행한 뒤,capacity와length값을 출력해 메모리가 어떻게 늘어나는지 기록합니다. - 확장하기:
realloc예제에서source를 사용자 입력으로 바꾸고, 줄의 길이가 0이면 바로 종료하도록 조건을 추가합니다. - 디버깅:
free호출을 일부러 주석 처리해 본 뒤,valgrind나AddressSanitizer같은 도구가 어떤 경고를 내는지 확인합니다(설치되어 있다면). - 완료 기준: "언제 할당하고 언제 해제하는지"를 글로 설명할 수 있고, 실패 시
NULL을 검사하는 패턴을 직접 작성할 수 있으면 됩니다.
마무리
동적 메모리는 "필요할 때 잠깐 빌려 쓰는 공간"입니다. 이번 편에서는 malloc 계열 함수 사용법과 free 습관을 익혔으니, 다음 편에서는 메모리 오류가 실제로 어떻게 나타나고 어떤 도구로 추적할 수 있는지 살펴보겠습니다.
💬 댓글
이 글에 대한 의견을 남겨주세요