지금까지 써 온 모든 배열은 크기가 컴파일 시점에 박혀 있었습니다. int scores[10];은 세 칸이 필요하든 삼백 칸이 필요하든 열 칸을 잡습니다. 그 방식은 크기를 프로그램이 돌아가는 동안에만 알 수 있을 때 한계에 부딪힙니다. 파일에 줄이 몇 개인지, 사용자가 레코드를 몇 개 입력할지 같은 것들이죠. 동적 메모리가 그 문제를 풉니다. 필요한 바이트를 필요한 때에 요청하고, 다 쓰면 돌려줍니다.
도구는 <stdlib.h>에 있습니다.
#include <stdlib.h>
힙이 존재하는 이유
C는 프로그램에 크게 두 개의 메모리 영역을 줍니다. 지역 변수는 자동 저장 영역(스택)에 놓입니다. 컴파일러가 크기를 정하고, 해당 함수가 반환되는 순간 사라집니다. 동적 할당은 힙에 놓입니다. 크기를 실행 시점에 여러분이 정하고, 명시적으로 해제할 때까지 그 블록은 살아 있습니다.
두 번째 성질이 malloc을 쓰는 진짜 이유입니다. 함수가 블록을 할당하고 포인터를 반환하면 그 메모리는 호출한 쪽에서도 여전히 유효합니다. 지역 배열로는 결코 할 수 없는 일이죠. 두 영역의 자세한 비교는 스택과 힙에 있습니다.
malloc: 바이트를 요청하기
malloc은 바이트 수를 받아서 그만큼의 초기화되지 않은 메모리를 가리키는 void *를 반환하고, 요청이 실패하면 NULL을 반환합니다.
이 짧은 프로그램의 네 가지가 이 분야의 규율 전부입니다.
- 크기 식은
n * sizeof *scores입니다. "scores가 가리키는 것 n개"로 읽으세요. - 사용하기 전에 결과를
NULL과 비교해 검사합니다. - 블록은 배열과 똑같이 사용합니다. 인덱싱이 곧 포인터 산술이므로
scores[i]가 동작합니다. - 정확히 한 번 해제하고, 그다음 포인터를
NULL로 둡니다.
반환값에 캐스트가 없다는 점에 주목하세요. C에서 void *는 어떤 객체 포인터로든 자동 변환되며, (int *)malloc(...)이라고 쓰면 잡음만 늘리면서 <stdlib.h> 인클루드 누락을 가려 버립니다. (C++는 캐스트를 요구하지만 C는 아닙니다.)
sizeof 관용구
왜 sizeof(int)가 아니라 sizeof *scores일까요? 낡을 수가 없기 때문입니다.
int *a = malloc(n * sizeof *a); /* int가 무엇이든 int n개 */
long *b = malloc(n * sizeof *b); /* 같은 모양의 줄, 알맞은 크기 */
/* 깨지기 쉬운 형태 */
long *c = malloc(n * sizeof(int)); /* 컴파일되지만 너무 적게 할당됨 */
마지막 줄은 어떤 컴파일러도 경고하지 않는 진짜 버그입니다. c는 long *인데 할당은 int 크기로 잡혀 있어서, 앞 절반을 넘어가는 모든 쓰기가 블록 밖으로 나갑니다. sizeof *c를 쓰면 크기가 선언을 자동으로 따라갑니다.
sizeof *p는 p를 역참조하지 않습니다. sizeof는 타입만 보고 컴파일 시점에 평가되므로, p가 초기화되지 않았거나 NULL이어도 안전합니다.
초기화되지 않은 메모리
malloc은 건네주는 메모리를 지워 주지 않습니다. 그 바이트에는 이전에 있던 값이 그대로 들어 있습니다.
블록을 0으로 채워서 받고 싶다면 calloc이 한 번에 해 줍니다. calloc과 realloc을 참고하세요.
free: 돌려주기
free(p)는 블록을 할당자에게 반납합니다. 규칙은 세 가지입니다.
malloc이 반환한 바로 그 포인터를 넘기세요. 앞으로 옮겨진 포인터는 안 됩니다.free(p + 1)은 미정의 동작입니다.- 각 블록은 정확히 한 번 해제하세요. 두 번 하면 double free이고, 할당자 자신의 장부를 망가뜨립니다.
- 해제한 뒤의 포인터는 낡은 값입니다. 그걸 사용하면 use-after-free입니다.
int *p = malloc(sizeof *p);
free(p);
*p = 5; /* use-after-free - 미정의 동작 */
free(p); /* double free - 미정의 동작 */
두 줄 다 즉시 크래시가 나야 할 의무는 없는데, 그래서 위험합니다. 프로그램이 몇 분을 잘 돌다가 전혀 관계없는 곳에서 죽을 수 있습니다. 값싼 방어책은 포인터를 비워 두는 것입니다.
free(p);
p = NULL;
free(NULL)은 아무 일도 하지 않도록 명시적으로 정의되어 있으므로 그 뒤의 두 번째 free(p)는 무해하고, *p는 조용한 손상 대신 진짜 문제 지점에서 즉시 크래시를 냅니다.
동적 배열, 처음부터 끝까지
실제 할당의 모양은 이렇습니다. 실행 중에 개수를 읽고, 그로부터 블록 크기를 정하고, 사용하고, 해제합니다.
개수에 붙인 size_t 캐스트는 크기가 커지면 중요해집니다. n이 평범한 int일 때 n * sizeof *values는 곱셈 결과가 malloc에 닿기도 전에 오버플로할 수 있고, 그러면 의도보다 훨씬 작은 블록이 만들어집니다. size_t에서 곱하면 그 문제를 피할 수 있습니다.
함수 안에서 할당하기
힙의 수명 규칙 덕분에 아래 코드가 합법입니다. 그리고 같은 규칙 때문에 지역 배열을 반환하는 것은 불법입니다.
len + 1은 널 종료 문자를 위한 자리를 남깁니다. 문자열에서 늘 쓰는 것과 같은 크기 규칙입니다. 그리고 함수 위의 주석에 주목하세요. 함수가 할당된 메모리를 반환할 때, 누가 그것을 해제하는지는 그 함수의 계약의 일부입니다. 그걸 적어 두는 것은 형식적인 서류 작업이 아니라 누수를 막는 유일한 수단입니다.
힙 위의 구조체
같은 관용구가 어떤 타입에든 통합니다.
두 번째 실패 분기가 반환하기 전에 p를 해제한다는 점에 주목하세요. 함수가 할당을 두 개 이상 쥐게 되면, 모든 오류 경로에서 이미 잡아 둔 것들을 놓아 주어야 합니다. 메모리 누수에서 설명하는 누수의 가장 흔한 원천입니다.
체크리스트
- 처음 사용하기 전에 항상
malloc의 결과가NULL인지 검사하세요. - 크기는
n * sizeof *p로 잡고, 곱셈은size_t에서 하세요. - C에서는 반환값에 캐스트를 붙이지 마세요.
malloc메모리는 초기화되어 있지 않습니다. 읽기 전에 쓰세요.- 원래 포인터로 정확히 한 번만 해제하고, 그다음
NULL로 두세요. - 할당 이후의 모든 오류 경로는 이미 쥐고 있는 것을 해제해야 합니다.
자주 묻는 질문
C에서 malloc은 무슨 일을 하나요?
malloc(n)은 힙에서 n바이트의 메모리를 요청하고 그 블록의 시작을 가리키는 포인터를 반환합니다. 요청을 들어줄 수 없으면 NULL을 반환합니다. 그 블록은 같은 포인터를 free에 넘길 때까지 살아 있습니다. 지역 변수와 달리 함수가 반환된다고 해제되지 않습니다.
C에서 malloc과 free는 어떻게 쓰나요?
int *p = malloc(n * sizeof *p);로 할당하고, 건드리기 전에 if (p == NULL)로 검사하고, 배열처럼 사용한 뒤, 다 쓰면 free(p);를 정확히 한 번 호출합니다. 그 후 p = NULL로 두면 나중에 실수로 사용했을 때 조용한 메모리 손상 대신 깔끔한 크래시가 납니다.
malloc(n * sizeof *p)가 malloc(n * sizeof(int))보다 나은 이유는 무엇인가요?
sizeof *p는 포인터의 타입을 자동으로 따라가기 때문입니다. 나중에 p가 long *이나 struct Point *가 되면 할당 크기도 알아서 바뀝니다. sizeof(int)라고 직접 적어 두면 이제는 틀린 크기가 남게 되고, 컴파일러는 그걸 지적해 주지 않습니다.
C에서 메모리를 해제하지 않으면 어떻게 되나요?
그 블록은 프로세스가 살아 있는 동안 계속 할당된 채로 남습니다. 메모리 누수입니다. 짧은 프로그램은 운영체제가 종료 시 모든 것을 회수하므로 넘어가지만, 오래 돌아가는 프로그램은 매 회차마다 조금씩 새다가 결국 메모리를 소진합니다. 메모리 누수를 참고하세요.