Menu

C의 calloc과 realloc: 0으로 채운 메모리와 배열 늘리기

calloc은 0으로 채운 메모리를 주고 크기 계산을 안전하게 해 줍니다. realloc은 이미 채워 둔 블록을 키워 줍니다. 이 페이지에서는 둘 다, 그리고 realloc 실패가 메모리 누수로 이어지지 않게 막는 임시 포인터 관용구와, calloc이 malloc + memset보다 나은 경우를 다룹니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

malloc은 한 가지 질문에 답합니다. 이만큼의 바이트를 달라는 것이죠. <stdlib.h>에 있는 두 동반자는 그다음에 오는 질문들에 답합니다. 이만큼의 바이트를, 깨끗이 비워서 달라(calloc), 그리고 이미 가진 블록이 더 커져야 한다(realloc)입니다.

calloc: 개수, 크기, 그리고 0

void *calloc(size_t count, size_t size);

인자가 하나가 아니라 둘이고, 결과는 0 바이트로 채워집니다.

전형적인 쓰임새는 이렇습니다. 모든 칸이 0에서 시작해야 하는 히스토그램이나 집계 배열이죠. malloc이라면 반복문이 필요하지만, calloc은 할당의 일부로 그 일을 해 주며 종종 공짜입니다. 운영체제가 이미 0으로 비워진 새 페이지를 내주기 때문입니다.

오버플로라는 근거

인자를 둘로 나눈 것은 겉치레가 아닙니다. 파일에서 읽어 온 개수를 생각해 보세요.

size_t n = huge_value_from_input;

int *a = malloc(n * sizeof *a);     /* 곱셈 결과가 넘칠 수 있음 */
int *b = calloc(n, sizeof *b);      /* 오버플로를 반드시 감지해야 함 */

n * sizeof *asize_t가 담을 수 있는 범위를 넘으면 값이 작은 수로 되돌아가고(wrap), malloc은 아주 작은 블록으로 성공하며, 그 뒤의 모든 쓰기가 끝을 한참 넘어갑니다. calloc은 표준상 이런 경우 실패하고 NULL을 반환해야 합니다. 크기가 프로그램 바깥에서 올 때 이 검사는 갖출 만한 가치가 있습니다.

"0으로 채운다"의 진짜 의미

calloc은 0 바이트를 씁니다. 정수와 문자에서는 그것이 값 0이고, 바로 여러분이 원하는 바입니다. 포인터와 부동소수점 값의 경우, 모든 비트가 0인 것은 주류 플랫폼 전부에서 널 포인터와 0.0이지만 C 표준이 그렇다고 보장하지는 않습니다. 엄격한 이식성이 필요한 코드는 그런 값들을 명시적으로 대입합니다.

calloc 대 malloc + memset

int *a = calloc(n, sizeof *a);

int *b = malloc(n * sizeof *b);
if (b != NULL) {
    memset(b, 0, n * sizeof *b);
}

두 방식의 결과는 같습니다. calloc을 선호하세요. 한 줄이고, 오버플로 검사를 해 주며, 큰 블록에서는 메모리를 아예 건드리지 않을 수도 있습니다. 어차피 모든 바이트를 덮어쓸 참이라면 순수한 malloc을 쓰세요. 곧바로 채울 1메가바이트를 0으로 미는 것은 완전한 낭비입니다.

realloc: 크기 바꾸기

void *realloc(void *p, size_t newsize);

reallocnewsize 바이트짜리 블록을 반환하며, 그 내용은 두 크기 중 작은 쪽까지 예전 블록과 같습니다. 블록을 있던 자리에서 늘릴 수도 있고, 새로 할당해 복사한 뒤 예전 것을 해제할 수도 있습니다. 어느 쪽인지 알 방법은 없으므로, 이후에 쓸 수 있는 포인터는 반환받은 것뿐입니다.

tmp 포인터 관용구

tmp 주변의 세 줄이야말로 이 함수가 악명을 얻은 이유 전부입니다. 솔깃한 축약형은 망가져 있습니다.

/* 버그: realloc이 실패하면 원래 블록이 누수됩니다 */
p = realloc(p, newsize);
if (p == NULL) {
    return;
}

realloc이 실패하면 NULL을 반환하고 원래 블록은 할당된 채 그대로 둡니다. 결과를 곧바로 p에 대입하면 그 블록을 가리키는 유일한 포인터를 덮어쓰게 되어 영영 해제할 수 없습니다. 누수이며, 하필 메모리가 이미 부족한 바로 그 순간에 일어나는 누수입니다. tmp에 대입하고, tmp를 검사한 뒤, 확정하세요.

void *tmp = realloc(p, newsize);
if (tmp == NULL) {
    /* p는 여전히 유효함; 호출자에게 맞는 방식으로 실패를 처리하세요 */
    return 0;
}
p = tmp;

알아 둘 만한 동작이 둘 더 있습니다. realloc(NULL, n)malloc(n)과 정확히 똑같이 동작하므로, 성장 함수가 첫 호출을 특별 취급 없이 처리할 수 있습니다. 그리고 크기를 늘리면 새로 생긴 바이트는 초기화되지 않습니다. 보존되는 것은 예전 내용뿐입니다.

배열 늘리기: 표준 패턴

거의 모든 C 컨테이너가 쓰는 패턴이 여기 있습니다. 길이, 용량, 그리고 둘이 만나면 두 배로 늘리기입니다.

여기서 무게를 지탱하는 설계 포인트는 둘입니다.

하나씩 더하지 말고 두 배로. 고정된 양만큼 늘리면 push할 때마다 배열 전체를 복사하게 되어, n개를 채우는 데 대략 번의 바이트 복사가 듭니다. 두 배로 늘리면 복사가 충분히 드물어져서 push 하나의 평균 비용이 상수 시간이 됩니다. cap == 0 ? 4 : cap * 2라는 시작은 별도 분기 없이 첫 push를 처리해 줍니다. realloc(NULL, n)이 곧 malloc이니까요.

구조체가 할당 하나를 소유합니다. v.data는 그 구조체를 소유한 쪽이 정확히 한 번만 해제합니다. 그 소유권 규칙을 글로 적어 두는 것(주석으로든, 모든 vec_initvec_free와 짝지어 두는 방식으로든)이 커지는 컨테이너가 함수들 사이를 오가면서도 누수되지 않게 지켜 줍니다.

크기 줄이기

더 작은 크기로 realloc하면 블록이 잘립니다. 크기를 넉넉히 잡은 버퍼에 양을 알 수 없는 데이터를 읽어 들인 뒤에 유용합니다.

축소 실패는 프로그램을 중단할 만한 오류가 아닙니다. 예전의 더 큰 블록이 여전히 유효하고 여전히 올바르니까요. 실패를 무시하는 것이 옳은 판단인 유일한 경우입니다. 다만 그래도 tmp를 거친다는 점에 주목하세요. 살아 있는 포인터를 덮어쓰지 말라는 규칙은 달라지지 않습니다.

무엇을 골라야 할까

원하는 것사용할 함수
바이트만 필요하고 내용은 상관없음malloc(n * sizeof *p)
항목 개수만큼, 0으로 비워서calloc(n, sizeof *p)
신뢰할 수 없는 입력에서 온 개수오버플로 검사를 위해 calloc
가진 블록을 더 크게 또는 더 작게tmp 포인터를 거친 realloc
성장 반복문의 첫 할당realloc(NULL, n) - 특별 처리 불필요

넷 모두 같은 free로, 정확히 한 번 해제됩니다.

흔한 실수

  • p = realloc(p, n) - 실패 시 누수됩니다. 항상 tmp를 거치세요.
  • 블록 내부를 가리키던 예전 포인터를 계속 쓰기. realloc 이후에는 데이터가 옮겨졌을 수 있으므로, 저장해 둔 내부 포인터나 인덱스로 계산해 둔 주소는 모두 낡은 값입니다. 이동해도 인덱스는 살아남지만 포인터는 그렇지 않습니다.
  • 새 바이트가 0이라고 가정하기. 0으로 채우는 것은 calloc뿐입니다. realloc은 늘어난 영역을 초기화하지 않습니다.
  • calloc(n * size, 1) - 인자를 둘로 나눈 형태가 존재하는 이유인 오버플로 검사를 스스로 내던지는 셈입니다.
  • realloc 성공 후에 예전 포인터를 해제하기. realloc이 이미 해제했으므로 두 번째 free는 이중 해제입니다.

자주 묻는 질문

C에서 calloc과 malloc의 차이는 무엇인가요?

두 가지입니다. calloc(n, size)는 개수와 요소 크기를 따로 받아서 둘을 곱했을 때 오버플로가 나지 않는지 검사하지만, malloc(n * size)는 조용히 값이 넘칠 수 있습니다. 그리고 calloc은 블록을 0으로 채우는 반면, malloc은 그 자리에 있던 값을 그대로 둡니다.

C에서 realloc은 어떻게 동작하나요?

realloc(p, newsize)는 새 크기의 블록을 반환하며, 기존 내용은 두 크기 중 작은 쪽까지 보존됩니다. 블록을 그 자리에서 키울 수도 있고 옮길 수도 있으므로, 이후에 계속 쓸 수 있는 포인터는 반환된 것뿐입니다. 예전 포인터는 이미 해제되었을 수 있습니다.

왜 realloc의 결과를 임시 포인터에 대입해야 하나요?

p = realloc(p, n)은 호출이 실패하면 pNULL로 덮어쓰는데, 원래 블록은 여전히 할당된 채 아무도 가리키지 않게 됩니다. 곧 메모리 누수죠. 먼저 tmp에 대입하고 NULL 여부를 확인한 뒤에만 p = tmp를 쓰세요.

malloc 대신 calloc은 언제 써야 하나요?

메모리가 정말로 0이어야 할 때(카운터 배열, 필드가 비어 있는 상태로 시작해야 하는 구조체, 일부만 채울 버퍼)이거나, 크기가 커질 수 있는 '개수 × 요소 크기' 형태일 때입니다. 어차피 모든 바이트를 덮어쓸 거라면 malloc이 0으로 채우는 작업을 건너뜁니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기