Subsections


7. Memory Allocation

많은 사람들이 C 언어에서 pointer가 가장 배우기 힘들다고 말합니다. 그러나 사실 이 말은 pointer보다 pointer가 가리키는 메모리를 관리하기가 힘들다는 뜻입니다. C 언어는 상대적으로 저수준의 언어이기 때문에, 프로그래머가 메모리를 직접 할당해야 합니다. 제대로 할당되지 않은 메모리를 가리키는 포인터를 쓰는 것은 심각한 버그를 발생하는 가장 주된 원인이 됩니다.

7.1 Basic Allocation Problems

여러분이 직접 malloc을 부르지 않는다해도, 여러분이 쓰려고 하는 메모리가 제대로 초기화되었는지 검사해야 합니다.



Q 7.1
이 코드는 왜 동작하지 않을까요?
  char *answer;
  printf("Type something:\n");
  gets(answer);
  printf("You typed \"%s\"\n", answer);

Answer
gets()에 전달된 포인터 변수인 answer는 문자열이 저장될 어떤 공간을 가리키고 있어야 합니다. 위의 코드에서는 answer가 어디를 가리키는 지 알 수 없습니다. 즉, 초기화되지 않는 변수이기 때문에 다음 코드와 같이 잘못된 코드입니다:

  int i;
  printf("i = %d\n", i);

따라서 주어진 코드의 첫 줄에서 우리는 answer가 어디를 가리키는지 알 수 없습니다. 즉, 바로 위의 코드에서 i가 무슨 값을 가지는지 알 수 없는 것과 같은 이유입니다. (초기화되지 않은 지역 변수7.1는 일반적으로 쓰레기 값을 가지게 됩니다. 이는 널 포인터와는 다릅니다. 질문 [*]1.30, [*]5.1을 참고하기 바랍니다)

위와 같이, 간단히 질문하고 답변받는 프로그램에서는 포인터 대신 지역 배열을 쓰는 것이 훨씬 간단합니다:

  #include <stdio.h>
  #include <string.h>

  char answer[100], *p;
  printf("Type something:\n");
  fgets(answer, sizeof answer, stdin);
  if ((p = strchr(answer, '\n')) != NULL)
    *p = '\0';
  printf("You typed \"%s\"\n", answer);

이 예제는 gets() 대신 fgets()를 썼습니다. 따라서 배열의 끝이 덮어쓰여질 염려가 없습니다. (질문 [*]12.23을 참고하기 바랍니다. 아쉽게도 이 예에서 쓴 fgets()gets()와 달리 마지막 \n을 지우지 못합니다.) answermalloc()으로 메모리를 할당하고, 버퍼 크기를 다음과 같이 파라메터로 넘기는 방법도 있습니다.

  #define ANSWERSIZE    100



Q 7.2
strcat()이 동작하지 않습니다. 코드는 다음과 같으며:
  char *s1 = "Hello, ";
  char *s2 = "world!";
  char *s3 = strcat(s1, s2);
이상한 결과가 발생합니다.

Answer
질문 [*]7.1에서 언급한 것처럼, 주 원인은 연결한 문자열을 저장할만한 공간이 없다는 것입니다. C 언어는 동적으로 문자열을 처리해주지 않습니다. C 컴파일러는 소스에서 요구한 만큼만 메모리를 할당합니다 (문자열의 경우엔 문자 배열과 문자열 상수를 포함합니다). 프로그래머는 문자열 연결(concatenation)과 같은 실행 시간 연산(run-time operation)에 필요한 적절한 공간을 배열이나 malloc()을 불러서 직접 만들어 주어야 합니다.

strcat()은 어떠한 공간도 할당해주지 않습니다; 두번째 문자열은 단순히 첫번째 문자열에 붙어서 연결됩니다. 그러므로 첫번째 문자열을 저장하는 곳이 충분한 공간을 가지고 있어야 하며, 쓸 수 있어야(writable) 합니다. 따라서 다음과 같이 배열로 선언하면 쉽습니다:

  char s1[20] = "Hello, ";

물론, 실전에 쓰기 위해서는 위와 같이 20이라는 상수를 쓰는 것은 좋지 않습니다. 우리는 적절한 공간을 보장할 수 있는 어떠한 메커니즘을 써서 공간을 할당해야 합니다.

strcat()이 첫번째 문자열을 가리키는 (질문에서는 s1) 포인터를 리턴하므로, 변수 s3는 불필요합니다; strcat()을 부른 다음에 결과는 s1이 가리키고 있습니다.

질문에서 strcat()을 부른 코드는 크게 두가지 문제를 가지고 있습니다: 먼저 s1이 충분한 공간을 가지고 있지 않다는 것과, 이 문자열이 쓸 수 없는(read-only) 문자열이라는 것입니다. 질문 [*]1.32를 참고하기 바랍니다.

References
[CT&P] § 3.2 p. 32



Q 7.3
그러나 매뉴얼(man) 페이지는 strcat()이 인자로 두 개의 char *를 받는다고 씌여 있습니다. 어떻게 이 것만 가지고 필요한 메모리 공간을 만들어 주어야 한다는 것을 알 수 있죠?

Answer
일반적으로 포인터를 사용할 때에는 항상 메모리 공간을 잡아 주어야 한다고 생각해야 합니다. 만약 라이브러리 함수의 설명에 메모리 할당에 대한 언급이 없다면, 대부분은 호출하는 쪽에서 알아서 해 주어야 합니다.

UNIX 스타일의 매뉴얼(man) 페이지의 첫 부분에 있는 `Synopsis' 섹션, 또는 ANSI C 표준에 나온 이러한 부분은 잘못 이해하기 쉽습니다. 이들 문서에 나온 코드는 호출하는 입장이 아닌 함수를 만드는 입장에 더 가깝기 때문입니다. 특히 포인터를 인자로 받는 (구조체나 문자열) 대부분의 함수들은 포인터가, 어떤 오브젝트를 (구조체나 배열 -- 질문 [*]6.3, [*]6.4 참고) 가리켜야 하는 경우가 많으며, 이 오브젝트는 부르는 입장에서 할당해 주어야 하는 경우가 대부분입니다. 다른 예로는 time() 함수와 (질문 [*]13.12 참고) stat() 함수를 들 수 있습니다.



Q 7.3b
다음과 같은 코드를 실행했는데:
  char *p;
  strcpy(p, "abc");
동작을 합니다. 왜 그러죠? 제 예상대로라면 프로그램이 망가져야(crash) 하는데요.

Answer
추측컨대 아주 재수가 좋은 모양입니다. 초기화되지 않은 포인터 p에 어떤 쓰레기 값이 들어갔고, 그 값이 여러분이 쓸 수 있는 메모리 공간을 가리키고 있었고, 그 공간이 어떤 중요한 목적으로 쓰이고 있지 않았기 때문입니다.



Q 7.3c
포인터 변수는 얼마나 큰 메모리를 할당할까요?
Answer
아주 잘못된 질문입니다. 포인터 변수를 다음과 같이 선언했다고 할 때:
  char *p;
여러분은 (좀더 정확히 말해서, 컴파일러는) 포인터 자체만 저장할 수 있는 공간을 할당한 것입니다; 즉, 이 경우 sizeof(char *) 바이트만큼의 메모리가 할당된 것입니다. 그러나 이 포인터는 아직 어떠한 메모리도 가리키고 있지 않습니다. 질문 [*]7.1과 [*]7.2를 참고하기 바랍니다.



Q 7.4
다음과 같이 파일에서 줄 단위로 읽는 코드를 만들었습니다:
  char linebuf[80];
  char *lines[100];
  int i;

  for (i = 0; i < 100; i++) {
    char *p = fgets(linebuf, 80, fp);
    if (p == NULL) break;
    lines[i] = p;
  }
그런데 이 코드를 실행하면, 모든 줄에 마지막 줄의 내용이 들어가 있습니다.

Answer
여러분이 선언한 linebuf는 단지 한 줄만을 저장할 수 있는 버퍼입니다. fgets를 부를 때마다, 기존의 줄은 덮어써져 버립니다. fgets는 내부적으로 메모리를 할당해 주지 않습니다. 에러가 났거나 EOF를 만나지 않는다면, fgets가 리턴하는 값은 여러분이 첫번째 인자로 전해 준 포인터와 같습니다. (이 경우 linebuf를 가리키는 포인터)

이런 식으로 코드를 작성하려 한다면, 여러분이 각각의 줄을 저장할 공간을 일일이 할당해 주어야 합니다. 질문 [*]20.2의 코드를 참고하기 바랍니다.

References
[K&R1] § 7.8 p. 155
[K&R2] § 7.7 pp. 164-5 [ANSI] § 4.9.7.2
[C89] § 7.9.7.2
[H&S] § 15.7 p. 356



Q 7.5a
문자열을 리턴하는 함수를 만들었는데, 리턴한 문자열이 쓰레기로 채워진 것 같습니다. 왜 그럴까요?
Answer
포인터가 적절한 메모리 공간을 가리키고 있는지 잘 검사해보시기 바랍니다. 함수가 리턴하는 포인터는 정적으로 할당된 버퍼이거나, 이 함수를 부른 위쪽에서 전달해 준 버퍼이거나, 또는 malloc으로 할당한 버퍼 등을 가리키고 있어야 합니다. 그러나 절대로 이 함수 안에서 할당한 지역 변수면 (automatic array) 안됩니다. 예를 들어 절대로 다음과 같이 하지 말기 바랍니다:

  #include <stdio.h>

  char *itoa(int n)
  {
    char retbuf[20];           /* WRONG */
    sprintf(retbuf, "%d", n);
    return retbuf;             /* WRONG */
  }
함수가 끝나면, 함수 안에서 만들어진 automatic, local 변수는 없어집니다. 따라서 위 경우는 올바른 코드가 아닙니다 (즉, 리턴되는 포인터가 존재하지 않는 곳을 가리키고 있습니다).

한 가지 방법은 (완전한 것은 아닙니다. 특히 이 함수가 재귀적으로 호출되거나 동시에 이 함수를 여러번 부르고, 그 값을 사용하려 할 때에는 쓸 수 없습니다.) 리턴할 버퍼를 다음과 같이 만드는 것입니다:

  static char retbuf[20];

다른 방법은, 이 함수를 부르는 함수가, 버퍼를 제공하도록 고치는 것입니다:

  char *itoa(int n, char *retbuf)
  {
    sprintf(retbuf, "%d", n);
    return retbuf;
  }

  ...

    char str[20];
    itoa(123, str);

또, malloc을 쓰는 방법도 있습니다:

  #include <stdlib.h>

  char *itoa(int n)
  {
    char *retbuf = malloc(20);
    if (retbuf != NULL)
      sprintf(retbuf, "%d", n);
    return retbuf;
  }

  ...

    char *str = itoa(123);

이 경우, 이 함수를 부르는 쪽에서 더 이상 이 함수가 리턴한 값이 필요없을 경우, 리턴한 포인터가 가리키고 있는 메모리를 free해 주어야 합니다.

덧붙여 질문 [*]7.5b, [*]12.21, [*]20.1도 참고하시기 바랍니다.

References
[ANSI] § 3.1.2.4
[C89] § 6.1.2.4



Q 7.5b
그럼 문자열이나 기타 이런 것들을 리턴하려면 어떻게 해야 하죠?
Answer
포인터를 리턴할 때에는 이 포인터가 정적으로 할당된 (statically-allcated) 공간을 가리키고 있어야 합니다. 또는 이 버퍼가 이 함수에 전달된 메모리를 가리키고 있거나, 이 버퍼가 malloc()으로 할당된 버퍼이어야 합니다. 절대로 지역(local, automatic) 배열을 가리키고 있어서는 안됩니다.

질문 [*]20.1을 참고하기 바랍니다.

7.2 Calling malloc

고정된 메모리 블럭을 넘어서, 좀 더 많은 유연성을 필요로 한다면, 메모리를 동적으로 할당하는 방식을 쓸 단계입니다. 대개는 malloc 함수를 써서 이 작업을 수행합니다. 이 section에서는 malloc에 대한 기본적인 사항에 대하여 다루며, 다음 section에서는 malloc이 실패했을 경우에 대한 것을 배웁니다.



Q 7.6
malloc()을 호출할 때 왜 “warning: assignment of pointer from integer lacks a cast”라는 경고가 발생할까요?
Answer
<stdlib.h>를 포함하거나, malloc을 쓰기 위해 올바른 선언을 제공했는지 확인하기 바랍니다. 만약 이런 일을 안했다면, 컴파일러는 int를 리턴하는 것으로 (질문 [*]1.25 참고) 가정합니다. 물론 이 가정은 잘못된 것입니다 (같은 이유로, 질문한 경고 메시지가 calloc이나 realloc에서 발생할 수 있습니다. 덧붙여 질문 [*]7.15도 참고하시기 바랍니다.
References
[H&S] § 4.7 p. 101



Q 7.7
어떤 코드를 보면 malloc()이 리턴한 포인터를 대입할 포인터의 타입으로 캐스팅한 것을 볼 수 있는 데, 왜 그럴까요?
Answer
ANSI/ISO C 표준에서 void *를 소개되기 전에는, 대개 포인터 변환에 관계된 경고를 없애기 위해, 또는 불필요한 변환을 줄이기 위해 이러한 캐스팅을 사용했습니다.

ANSI/ISO C 표준에서는 이러한 캐스팅이 전혀 필요없습니다. 그리고 현재 이런 캐스팅을 사용하는 것은 나쁜 프로그래밍 스타일로 간주되기도 합니다. 왜냐하면 malloc()이 선언되지 않았을 때 발생할 수 있는 유용한 경고 메시지를 발생시키지 않기 때문입니다; 질문 [*]7.6을 참고하기 바랍니다. (그러나 이런 캐스팅은 여전히 자주 쓰이고 있습니다. 왜냐하면 C++에서는 이러한 캐스팅이 반드시 필요하기 때문에, 호환성을 유지하기 위해서입니다.)

References
[H&S] § 16.1 pp. 386-7



Q 7.8
다음과 같은 코드를 본 적이 있습니다:
  char *p = malloc(strlen(s) + 1);
  strcpy(p, s);
제 생각에는 malloc((strlen(s) + 1) * sizeof(char))로 되어야 할 것 같은데요.
Answer
sizeof(char)로 곱하는 것은 전혀 필요 없습니다. 왜냐하면 정의에 의해, sizeof(char)는 항상 1이기 때문입니다. 다른 말로 하면 sizeof(char)를 곱하는 것은 전혀 문제가 되지 않습니다. 1을 곱하는 것은 아무런 영향을 끼치지 않기 때문입니다. 추가적으로 size_t 타입을 쓰는 것이 도움이 될 때도 있습니다. (질문 [*]7.15 참고) 덧붙여 질문 [*]8.9, [*]8.10도 참고하시기 바랍니다.
References
[ANSI] § 3.3.3.4
[C89] § 6.3.3.4
[H&S] § 7.5.2 p. 195



Q 7.9
다음과 같이 malloc()의 wrapper를 만들었습니다. 그런데 왜 동작하지 않을까요?
  #include <stdio.h>
  #include <stdlib.h>

  mymalloc(void *retp, size_t size)
  {
    retp = malloc(size);
    if (retp == NULL) {
      fprintf(stderr, "out of memory\n");
      exit(EXIT_FAILURE);
    }
  }
Answer
질문 [*]4.8을 참고하기 바랍니다. (이 경우, 여러분은 mymalloc이 할당된 공간을 가리키는 포인터를 돌려주도록 해야 합니다.)



Q 7.10
포인터를 선언하고 여기에 공간을 할당하려고 합니다. 그런데 제대로 동작하지 않는군요. 다음 코드에서 뭐가 잘못되었나요?
  char *p;
  *p = malloc(10);
Answer
질문 [*]4.2를 보기 바랍니다.



Q 7.11
배열을 동적으로 할당할 수 있을까요?
Answer
질문 [*]6.14와 [*]6.16을 보기 바랍니다.



Q 7.12
쓸 수 있는 메모리 공간이 얼마나 남아 있는지 알 수 있을까요?
Answer
질문 [*]19.22를 보기 바랍니다.



Q 7.13
malloc(0)은 어떤 값을 리턴하는 거죠? 널 포인터를 리턴하나요, 0 바이트를 가리키는 포인터를 리턴하나요?
Answer
질문 [*]11.26을 보기 바랍니다.



Q 7.14
어떤 시스템에서는 malloc()으로 메모리를 할당해도 프로그램에서 이 메모리에 접근하기 전에는 운영체제가 메모리를 할당하지 않는다고 들었습니다. 그래도 상관없을까요?
Answer
꽤 말하기 어려운 문제입니다. 표준에서는 이런 식으로 동작하는 시스템이 있다고 말한 바가 없습니다. 하지만, 반대로 그러한 시스템이 없다고말한 바도 없습니다. (이와 같은 “deferred failure” implementation은 표준이 요구하는 것과 상관없어 보입니다.)

실제 메모리에 쓰려고(write) 할 때, 남아 있는 메모리가 없는 경우가 문제가 되는데, C 언어 semantic에는 recourse가 없으므로, 이 경우, 대개 OS가 해당 프로그램을 죽이게(kill) 됩니다. (당연히, malloc은 메모리가 없을 경우, 널 포인터를 리턴하게 되므로, malloc의 리턴 값을 확인했다면, 할당한 메모리가 아닌 다른 메모리에 접근할 까닭이 없습니다.)

이와 같이 “lazy allocation” 방식을 쓰는 시스템에서는 대개 메모리가 부족하다는 것을 알려주는 시그널을 제공합니다. 그러나 프로그램에서 이 시그널을 처리하려면 이식성이 떨어지는 프로그램이 될 가능성이 높습니다. 이 방식을 제공하는 시스템 중에는 사용자 또는 프로세스 단위로 이 기능을 끄는(off) 기능을 제공하는 (즉, 전형적인 malloc semantic을 쓰는) 것도 있습니다만, 시스템마다 매우 달라집니다.

References
[ANSI] § 4.10.3
[C89] § 7.10.3

7.3 Problems with malloc



Q 7.15
malloc이 이상한 값을 리턴할까요? 질문 [*]7.6을 읽고나서 malloc을 부르기 전에 extern void *malloc(); 선언을 포함시켰는데도 이상한 쓰레기 값을 줍니다.
Answer
malloc의 인자 타입은 size_t이며, 아마 unsigned long 타입일 가능성이 높습니다. 만약에 int 타입(또는 unsigned int)을 전달했다면, malloc이 (깨진) 쓰레기 값을 받았을 가능성이 있습니다. (또는 long 타입을 전달했는데 size_tint일 경우에도 이럴 가능성이 있습니다.)

일반적으로, 표준 라이브러리 함수의 선언을 extern으로 직접 써 주는 것보다 알맞는 헤더 파일을 포함시키는 것이 훨씬 더 안전합니다. 덧붙여 질문 [*]7.16도 참고하시기 바랍니다.

비슷하면서 잘 알려진 또 다른 문제는, size_t 타입의 값을 (sizeof의 값 포함) printf%d를 써서 출력하는 것입니다. 이식성이 높은 코드를 얻기 위해서, 주어진 값을 직접 (unsigned long으로) 캐스팅한 다음 %lu 포맷으로 출력하는 것이 좋습니다.

  printf("%lu\n", (unsigned long)sizeof(int));
덧붙여 질문 [*]15.3도 참고하시기 바랍니다..
References
[ANSI] § 4.1.5, § 4.1.6
[C89] § 7.1.6, § 7.1.7
Note
C99 표준은 size_t를 쉽게 출력하기 위해, printf, scanf 함수들에 z modifier를 추가했습니다. 질문 [*]13.1을 참고하기 바랍니다.



Q 7.16
어떤 수학 계산을 하기 위해 꽤 큰 배열이 필요해서 다음과 같이 코드를 작성했습니다:
  double *array = malloc(256 * 256 * sizeof(double));
이 때 malloc()은 널 포인터를 리턴하지 않았지만 프로그램이 매우 이상하게 동작합니다. 제 생각에는 메모리를 겹쳐 쓰는(overwrite) 현상이 발생한 것 같거나 malloc()이 원하는 만큼의 큰 메모리를 할당한 것 같지가 않습니다.
Answer
256 곱하기 256은 65,536이며, int가 16-bit인 경우, 저장될 수 없는, 큰 값입니다. 게다가 이 수치는 다시 sizeof(double)로 곱해야 하기 때문에 상당히 큰 크기입니다. 이런 큰 배열이 꼭 필요하다면 꽤 주의를 기울여야 합니다. 만약 여러분의 시스템에서 size_t 타입이 (malloc()이 인자로 받는 타입) 32 비트라면, 그리고 int가 16 비트라면, 위 코드에서 malloc의 인자로 256 * (256 * sizeof(double))를 써서 해결할 가능성도 있습니다. (질문 [*]3.14 참고). 그렇지 않다면 여러분은 이 메모리를 작은 크기로 쪼개어 쓰거나, 또는 32 비트 컴퓨터나 컴파일러를 쓰거나, 비표준으로 제공되는 메모리 할당 함수를 써야 할 것입니다. 덧붙여 질문 [*]7.15, [*]19.23도 참고하시기 바랍니다.

Note
이 질문은 16-bit 시스템 및 컴파일러가 대부분일 시절에 만들어진 것이기 때문에, 현실과 거리가 멀 수도 있지만, 비슷한 이유로 32-bit 시스템에서도 문제가 발생할 수 있으므로 그냥 두겠습니다.



Q 7.17
제 PC는 8 메가 바이트의 메모리를 보유하고 있습니다. 그런데 왜 640K의 크기 밖에 쓸 수 없는 것일까요?
Answer
PC 호환의 세그먼트(segment) 구조를 사용하는 컴퓨터에서 640K 이상의 메모리를 사용하는 것은 (특히 MS-DOS에서) 매우 어렵습니다. 질문 [*]19.23을 참고하기 바랍니다.



Q 7.18
제 프로그램은 어떤 노드(어떤 자료 구조)를 동적으로 할당하는 작업을 매우 많이 수행합니다. 테스트 결과 malloc/free에서 발생하는 오버헤드가 bottleneck이 되고 있는데, 좋은 방법이 있을까요?
Answer
만약 주어진 노드의 크기가 모두 같다면, free를 부르는 대신에 개발자가 직접 쓰이지 않는 노드로 이루어진 리스트를 관리하는 것이 좋을 수 있습니다. (이 방식은, 프로그램에서 가장 많은 메모리를 소비하는 것이 바로 이 노드일 때 좋은 해결책이 됩니다. 그렇지 않다면 이 노드 리스트에 묶여 있는 메모리가 많아서 다른 곳에서 메모리를 쓸 수 없게 되어 버립니다.)
Note
사용자가 직접 죽은(dead) 노드들을 리스트에 저장해 두고, 필요할 때, malloc을 부르는 대신 이 죽은 노드를 활용하는 것을, `object caching'이라고 부르기도 합니다.

예를 들어, 리스트의 노드가 다음과 같을 경우:

  struct node {
    struct node *next;
    ...
  };
그리고, 새 노드를 만드는 함수와 필요없는 노드를 삭제하는 함수가 다음과 같다고 가정합시다:
  struct node *new_node(void)
  {
    struct node *p;
    p = malloc(sizeof(*p));
    /* check if malloc() returns a null pointer */
    ...
    return p;
  }

  void delete_node(struct node *p)
  {
     ...
     free(p);
  }
이 경우, 다음과 같은 전역 변수를 두고,
  static struct node *grave = 0;
위에서 만든 두 함수를 “object caching” 기법을 쓰도록 바꿀 수 있습니다:
  struct node *new_node(void)
  {
    struct node *p;
    if (grave) {
      p = grave;
      grave = p->next;
    }
    else {
      p = malloc(sizeof(*p));
      /* check if malloc() returns a null pointer */
    }
    ...
    return p;
  }

  void delete_node(struct node *p)
  {
     ...
     p->next = grave;
     grave = p;
  }
물론 수정된 두 함수가 완벽하지는 않습니다. delete_node()free를 부르지 않고, 필요없는 모든 nodegrave 포인터에 리스트로 보관하고, 필요한 경우, (즉 new_node()를 불렀을 경우) 리스트에 보관된 노드를 다시 씁니다. 최적화를 위해서, 일정한 갯수만 grave 리스트에 보관하고, 나머지는 free를 불러서 해제하는 방식으로도 만들 수 있습니다.

이런 “object caching”이 모든 문제를 해결해 주지는 않습니다. 상황에 따라, 이 방식이 도움이 될 경우가 있으므로, 여기에서 소개한 것이지, 항상 좋다는 것은 아니라는 것을 기억하시기 바랍니다.



Q 7.19
제 프로그램은 malloc() 내부에서 오류가 발생한 것 같습니다. 그런데 제가 보기에는 제 프로그램에 잘못된 부분이 없는 것 같습니다. malloc() 내부에 버그가 있는 게 아닐까요?
Answer
불행히도, 프로그래머가 malloc()이 내부적으로 유지하는 데이터 구조를 망가뜨리는 경우가 자주 발생합니다. 또 그 결과 생기는 문제는 매우 다루기가 어렵습니다. 가장 흔히 발생하는 실수는, malloc()으로 할당한 메모리보다 더 많은 데이터를 쓰려고(write) 하는 경우입니다; 특히 문자열을 저장하기 위해 strlen(s) + 1을 쓰지 않고 malloc(strlen(s))를 쓰는 경우가 흔합니다.7.2 다른 문제로 이미 free()를 불러서 반환한 메모리 블럭에 쓰려고(write) 하거나 (질문 [*]7.30 참고), free()를 같은 포인터에 대해 두 번 호출하는 경우도 있습니다. (이 중 몇가지는 표준에 의해 인정되고 있습니다. ANSI 호환 시스템에서 크기가 0인 메모리 블럭을 malloc, realloc 등으로 할당하거나, 널 포인터를 free()로 반환하는 것은 아무런 문제도 일으키지 않습니다. 물론 오래된 시스템에서는 문제가 됩니다.) 대개 이러한 실수는 금방 발견되지 않고, 오랫동안 숨어 있거나, 또는 전혀 상관없는 코드 부분에서 에러가 발생한 것처럼 보이기도 해서 수정하기가 매우 까다롭습니다.

대부분 malloc implementation은 중요한 정보를 할당한 메모리 바로 옆에 보관하기 때문에, 사용자가 포인터를 조금이라도 잘못 쓰게 되면, 심각한 문제가 발생합니다.

덧붙여 질문 [*]7.15, [*]7.26, [*]16.8, [*]18.2도 참고하시기 바랍니다.

7.4 Freeing Memory

Memory allocated with malloc can persist as long as you need it. It is never deallocated automatically (except when your program exits; see question [*]7.24). When your program uses memory on a transient basis, it can--and should--recycle it by calling free.



Q 7.20
동적으로 할당한 메모리를 해제한 다음에는 다시 쓸 수 있나요?
Answer
쓸 수 없습니다. 어떤 오래된 malloc()에 대한 문서는 해제된 메모리는 변경되지 않는 상태로 남아 있다고 (left undisturbed) 씌여 있지만 이는 잘못된 것이며, C 표준에도 언급되지 않은 사항입니다.

대부분의 프로그래머들이 반환된 메모리의 내용을 일부러 다시 쓰지는 않습니다만, 실수로 반환한 메모리 공간을 쓰기도 합니다. 다음에 나온 singly-linked 리스트를 반환하는 코드를 보시기 바랍니다:

  struct list *listp, *nextp;
  for (listp = base; listp != NULL; listp = nextp) {
    nextp = listp->next;
    free(listp);
  }
만약 위 코드가 임시 변수 nextp를 쓰지 않고, listp = listp->next를 썼다면, 이미 반환한 메모리를 listp->next로 다시 접근하려 한다는 것을 알 수 있습니다.
References
[K&R2] § 7.8.5 p. 167
[ANSI] § 4.10.3
[C89] § 7.10.3
[ANSI Rationale] § 4.10.3.2
[H&S] § 16.2 p. 387
[CT&P] § 7.10 p. 95



Q 7.21
free()를 부른 다음에 포인터가 널이 되지 않는 걸까요?
Answer
free()를 부르면 이 함수에 전달된 포인터가 가리키고 있던 메모리가 해제됩니다. 그러나 이 포인터 자체의 값은 변경되지 않고 남아있습니다. 왜냐하면 C 언어는 인자를 전달할 때, `pass-by-value' 개념을 쓰기 때문입니다. 따라서 함수가 (이 경우 free()) 인자로 전달된 변수의 값을 변경할 수 없습니다. (질문 4.8을 참고하기 바랍니다.)

일단 해제된 포인터 값은 엄밀히 말해서, 유효하지 않습니다(invalid). 그리고 (dereference가 아니더라도) 어떠한 목적으로 (심지어, 단순히 대입하거나 비교하는 것도) 이 값을 쓰는 것은, 물론 구현 방법에 따라 다르긴 하지만, 이론상 문제를 발생할 수 있습니다. (물론, 대부분의 시스템이 문제가 없어 보이는 invalid 포인터 쓰임새를 너그럽게 봐 주지만, 표준은 확실하게, 어떤 것도 보장될 수 없다고 말합니다. 또한 어떤 시스템은 구조상 이런 exception 상황이 흔히 발생합니다.)

References
[ANSI] § 4.10.3
[C89] § 7.10.3
[ANSI Rationale] § 3.2.2.3



Q 7.22
함수에 속한 지역(local) 포인터에 malloc()으로 메모리를 할당했을 때에도 반드시 free()를 해주어야 하나요?
Answer
당연합니다. 포인터와 포인터가 가리키는 메모리는 서로 다른 것이라는 것을 기억해야 합니다. 함수가 끝났을 때, 지역 변수로 선언한 포인터는 자동으로 해제되지만, 포인터 자체가 해제된다는 뜻이지, 포인터가 가리키는 메모리 블럭이 해제된다는 것은 아닙니다. malloc()으로 할당한 메모리는 free()를 써서 해제하기 전에는 메모리에 남아 있습니다. 일반적으로 모든 malloc()에는 각각의 호출에 해당하는 free()를 만들어 주어야 합니다.



Q 7.23
동적으로 할당한 어떤 오브젝트를 가리키는 포인터를 포함하는 구조체를 할당했습니다. 이 구조체를 해제할 때, 각각의 포인터 멤버가 가리키는 메모리도 따로 해제해 주어야 하나요?
Answer
그렇습니다. malloc()으로 할당한 메모리는 각각, 정확히 딱 한번, free() 해주어야 합니다. 프로그램에서 각각 malloc()으로 할당한 메모리는 모두 free()시키는 것이 좋은 습관입니다.

질문 [*]7.24를 참고하기 바랍니다.



Q 7.24
프로그램이 끝나기 전에 동적으로 할당한 메모리는 반드시 해제시켜야 하나요?
Answer
그럴 필요는 없습니다. 일반적으로 운영체제는 프로그램이 끝났을 때, 프로그램이 할당한 모든 메모리를 해제시켜 줍니다. 그럼에도 불구하고, 어떤 PC에서는 메모리를 제대로 복원시키지 못한다고 알려져 있습니다. ANSI/[C89] C 표준에서는 이런한 상황을 `구현 수준에 따른 상황(quality of implementation issue)'이라고 말하고 있습니다.
References
[C89] § 7.10.3.2



Q 7.25
제 프로그램은 메모리 블럭을 malloc()으로 할당하고, 나중에 free()시키는 구조를 가집니다. 그런데, 프로그램이 끝난 다음에도, 운영 체제의 메모리 상태를 살펴보면 전혀 줄어들지 않는 것을 볼 수 있습니다.
Answer
대부분의 malloc/free의 구현 방법들은 메모리를 운영체제에 즉시 반환하는 것이 아니며, 단지 그 프로그램에서 나중에 나올 malloc()이 그 메모리를 다시 쓸 수 있도록 해 주도록 되어 있습니다.

7.5 Sizes of Allocated Blocks



Q 7.26
free() 함수는 어느 정도 크기의 메모리를 해제할 지 어떻게 알 수 있는 것일까요?
Answer
malloc/free 구현은 동적으로 할당한 각각의 메모리 블럭의 크기를 내부적으로 유지하고 있습니다. 따라서 free()를 쓸 때에 그 크기를 지정하지 않아도 자동으로 그 크기를 알 수 있습니다.



Q 7.27
그렇다면, 할당한 블럭의 크기가 어느 정도인지 malloc 패키지를 써서 알 수 있을까요?
Answer
불행하게도, 동적 블럭의 크기를 알 수 있는 표준 또는 호환성있는 방법은 제공되지 않습니다.



Q 7.28
어떤 포인터가 메모리를 가리키고 있을 때, 왜 sizeof는 그 메모리 블럭의 크기를 알려주지 않나요?
Answer
sizeof 연산자는 포인터가 가리키는 값이 malloc으로 할당한 메모리를 가리키고 있는지 알지 못합니다. sizeof가 알려주는 것은 포인터 자체가 차지하고 있는 크기입니다. malloc을 불러서 얻은 메모리 블럭의 크기를 알아내는 (이식성이 높은) 방법은 없습니다.

7.6 Other Allocation Functions



Q 7.29
(질문 [*]6.14를 따라서) 배열을 동적으로 할당한 다음, 이 배열의 크기를 바꿀 수 있을까요?
Answer
물론입니다. realloc이 바로 그 역할을 해 주는 함수입니다. (예를 들어, 질문 [*]6.14에 나온 것처럼 dynarray) 동적으로 할당된 배열의 크기를 바꾸려면 다음과 같은 코드를 씁니다:
  dynarray = (int *)realloc((void *)dynarray,
                            20 * sizeof(int));
realloc이 항상 메모리 블럭의 크기를 늘리는데7.3 쓰이는 것은 아닙니다. realloc은, 가능하면, 전달받은 인자와 같은 포인터 값을 돌려주지만, 요청한 크기에 맞게 메모리 블럭을 다시 찾을 경우에는, 인자로 전달된 포인터 값과는 다른 값을 돌려줍니다. 이 경우, 인자로 전달된 포인터 값은 더 이상 쓸 수 없습니다.

만약 realloc이 요청한 메모리 공간을 찾지 못했다면, 널 포인터를 리턴합니다. 이 때, 인자로 전달되었던 메모리는 (realloc을 부르기 바로 전 상태로) 그대로 유지됩니다.7.4

realloc을 써서 메모리의 크기를 변경했을 경우에, 다른 포인터가 이 메모리 공간을 가리키고 있었는지 (“alias”라는 용어를 씁니다) 주의해야 합니다: 만약에 realloc이 다른 곳에 메모리 블럭을 할당했다면, 다른 포인터들도 값이 올바르게 바뀌어야 합니다. (malloc의 실패 여부를 확인하지 않았다는 단점이 있긴 하지만) 누군가가 제공한 코드입니다:

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

  char *p, *p2, *newp;
  int tmpoffset;

  p = malloc(10);
  strcpy(p, "Hello,");    /* p is a string */
  p2 = strchr(p, ',');    /* p2 points into that string */

  tmpoffset = p2 - p;
  newp = realloc(p, 20);
  if (newp != NULL) {
    p = newp;                   /* p may have moved */
    p2 = p + tmpoffset;         /* relocate p2 as well */
    strcpy(p2, ", world");
  }

  printf("%s\n", p);
위와 같이 기본값(base)을 기초로 해서 포인터 값을 다시 계산하는 것이 바람직합니다. 다른 방법은-두 값의 차(difference)인 newp - p의 값을 기초로, realloc을 부르기 전, 후의 베이스 포인터의 값을 쓰는 방식- 동작한다고 보장할 수 없습니다. 왜냐하면, 포인터 뺄셈은 같은 오브젝트를 가리키는 포인터 사이에서만 의미가 있기 때문입니다. 덧붙여 질문 [*]7.12, [*]7.30도 참고하시기 바랍니다..
References
[K&R2] § B5 p. 252
[ANSI] § 4.10.3.4
[C89] § 7.10.3.4
[H&S] § 16.3 pp. 387-8



Q 7.30
realloc 함수의 첫번째 인자로 널 포인터를 전달하는 것이 안전한가요? 그리고 왜 그런 일을 하는 것이죠?
Answer
ANSI C 표준은 그런 식으로 써도 (그리고 realloc(..., 0)처럼 써서 메모리를 해제하는 것) 괜찮다고 말합니다. 그러나 대부분의 오래된 구현 방법에서는 이러한 사용법을 제공하지 않습니다. 따라서 표준이기는 하지만 완전히 호환성을 갖춘 방법이 아닙니다. realloc의 첫 인자로 널 포인터를 전달하면 메모리를 증가적으로 동적으로 할당하는 알고리즘을 (self-starting incremental allocation algorithm) 만들기 쉽습니다.
References
[C89] § 7.10.3.4
[H&S] § 16.3 p. 388



Q 7.31
calloc()malloc()의 차이는 무엇인가요? calloc()이 메모리를 0으로 만드는 것을 믿고 쓸 수 있나요? 그리고, calloc()으로 할당한 메모리를 free()를 써서 해제할 수 있나요? 아니면 cfree()와 같은 것을 사용하나요?
Answer
calloc()은 다음의 코드를 수행하는 것과 같습니다:
  p = malloc(m * n);
  memset(p, 0, m * n);
0으로 채운다는 것은 할당한 메모리의 모든 비트를 0으로 채운다는 뜻입니다. 따라서 이 값이 널 포인터가 아닐 수도 있으며(여기에 관한 것은 5 절을 참고하기 바랍니다.) 실수(floating-point)로 0이 아닐 수도 있습니다. 그리고 calloc()으로 할당한 메모리를 해제할 때에도 free()를 씁니다.
References
[C89] § 7.10.3부터 7.10.3.2
[H&S] § 16.1 p. 386, § 16.2 p. 386
[PCS] § 11 pp. 141-142



Q 7.32
alloca() 함수는 어떤 함수이며, 왜 쓰지 말라고 하죠?
Answer
alloca()는 이 함수를 호출한 함수가 끝날 때, 자동으로 할당한 메모리를 해제해 주는 함수입니다. 즉, alloca로 할당한 메모리는 특정 함수의 “스택 프레임(stack frame)”이나 문맥(context)에 종속적입니다.

alloca() 함수는 이식성있게 만들 수가 없습니다. 그리고 일반적인 스택을 사용하지 않는 컴퓨터에서는 매우 만들기 어려운 함수입니다. 특히 alloca로 할당한 메모리를 리턴하는 경우, 심각한 문제가 발생할 수 있습니다. 예를 들면:

  fgets(alloca(100), 100, stdin);

이런 이유에서 alloca 함수는 표준 함수가 아니며, 높은 이식성이 요구되는 프로그램에서는 (매우 쓸모 있기는 하지만) 쓸 수 없습니다.

질문 [*]7.22를 참고하기 바랍니다.

References
[ANSI Rationale] § 4.10.3

Seong-Kook Shin
2018-05-28