많은 사람들이 C 언어에서 pointer가 가장 배우기 힘들다고 말합니다. 그러나 사실 이 말은 pointer보다 pointer가 가리키는 메모리를 관리하기가 힘들다는 뜻입니다. C 언어는 상대적으로 저수준의 언어이기 때문에, 프로그래머가 메모리를 직접 할당해야 합니다. 제대로 할당되지 않은 메모리를 가리키는 포인터를 쓰는 것은 심각한 버그를 발생하는 가장 주된 원인이 됩니다.
char *answer; printf("Type something:\n"); gets(answer); printf("You typed \"%s\"\n", 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
을 지우지 못합니다.)
answer에
malloc()으로 메모리를 할당하고, 버퍼 크기를 다음과 같이
파라메터로 넘기는 방법도 있습니다.
#define ANSWERSIZE 100
char *s1 = "Hello, "; char *s2 = "world!"; char *s3 = strcat(s1, s2);이상한 결과가 발생합니다.
strcat()은 어떠한 공간도 할당해주지 않습니다; 두번째 문자열은 단순히 첫번째 문자열에 붙어서 연결됩니다. 그러므로 첫번째 문자열을 저장하는 곳이 충분한 공간을 가지고 있어야 하며, 쓸 수 있어야(writable) 합니다. 따라서 다음과 같이 배열로 선언하면 쉽습니다:
char s1[20] = "Hello, ";
물론, 실전에 쓰기 위해서는 위와 같이 20이라는 상수를 쓰는 것은 좋지 않습니다. 우리는 적절한 공간을 보장할 수 있는 어떠한 메커니즘을 써서 공간을 할당해야 합니다.
strcat()이 첫번째 문자열을 가리키는 (질문에서는 s1) 포인터를 리턴하므로, 변수 s3는 불필요합니다; strcat()을 부른 다음에 결과는 s1이 가리키고 있습니다.
질문에서 strcat()을 부른 코드는 크게 두가지 문제를 가지고 있습니다: 먼저 s1이 충분한 공간을 가지고 있지 않다는 것과, 이 문자열이 쓸 수 없는(read-only) 문자열이라는 것입니다. 질문 1.32를 참고하기 바랍니다.
char *
를 받는다고 씌여 있습니다. 어떻게 이 것만 가지고
필요한 메모리 공간을 만들어 주어야 한다는 것을 알 수 있죠?
UNIX 스타일의 매뉴얼(man) 페이지의 첫 부분에 있는 `Synopsis' 섹션, 또는 ANSI C 표준에 나온 이러한 부분은 잘못 이해하기 쉽습니다. 이들 문서에 나온 코드는 호출하는 입장이 아닌 함수를 만드는 입장에 더 가깝기 때문입니다. 특히 포인터를 인자로 받는 (구조체나 문자열) 대부분의 함수들은 포인터가, 어떤 오브젝트를 (구조체나 배열 -- 질문 6.3, 6.4 참고) 가리켜야 하는 경우가 많으며, 이 오브젝트는 부르는 입장에서 할당해 주어야 하는 경우가 대부분입니다. 다른 예로는 time() 함수와 (질문 13.12 참고) stat() 함수를 들 수 있습니다.
char *p; strcpy(p, "abc");동작을 합니다. 왜 그러죠? 제 예상대로라면 프로그램이 망가져야(crash) 하는데요.
char *p;여러분은 (좀더 정확히 말해서, 컴파일러는) 포인터 자체만 저장할 수 있는 공간을 할당한 것입니다; 즉, 이 경우
sizeof(char *)
바이트만큼의 메모리가 할당된 것입니다.
그러나 이 포인터는 아직 어떠한 메모리도 가리키고 있지 않습니다.
질문
7.1과
7.2를 참고하기 바랍니다.
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; }그런데 이 코드를 실행하면, 모든 줄에 마지막 줄의 내용이 들어가 있습니다.
이런 식으로 코드를 작성하려 한다면, 여러분이 각각의 줄을 저장할 공간을 일일이 할당해 주어야 합니다. 질문 20.2의 코드를 참고하기 바랍니다.
#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해 주어야 합니다.
<stdlib.h>
를 포함하거나, malloc을 쓰기 위해 올바른
선언을 제공했는지 확인하기 바랍니다. 만약 이런 일을 안했다면,
컴파일러는 int를 리턴하는 것으로 (질문
1.25 참고) 가정합니다.
물론 이 가정은 잘못된 것입니다 (같은 이유로, 질문한 경고 메시지가
calloc이나 realloc에서 발생할 수 있습니다.
덧붙여 질문
7.15도 참고하시기 바랍니다.
ANSI/ISO C 표준에서는 이러한 캐스팅이 전혀 필요없습니다. 그리고 현재 이런 캐스팅을 사용하는 것은 나쁜 프로그래밍 스타일로 간주되기도 합니다. 왜냐하면 malloc()이 선언되지 않았을 때 발생할 수 있는 유용한 경고 메시지를 발생시키지 않기 때문입니다; 질문 7.6을 참고하기 바랍니다. (그러나 이런 캐스팅은 여전히 자주 쓰이고 있습니다. 왜냐하면 C++에서는 이러한 캐스팅이 반드시 필요하기 때문에, 호환성을 유지하기 위해서입니다.)
char *p = malloc(strlen(s) + 1); strcpy(p, s);제 생각에는
malloc((strlen(s) + 1) * sizeof(char))
로
되어야 할 것 같은데요.
sizeof(char)
로 곱하는 것은 전혀 필요 없습니다.
왜냐하면 정의에 의해, sizeof(char)
는 항상 1이기 때문입니다.
다른 말로 하면 sizeof(char)
를 곱하는 것은 전혀 문제가
되지 않습니다. 1을 곱하는 것은 아무런 영향을 끼치지 않기 때문입니다.
추가적으로 size_t
타입을 쓰는 것이 도움이 될 때도 있습니다.
(질문
7.15 참고) 덧붙여 질문
8.9,
8.10도 참고하시기 바랍니다.
#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); } }
char *p; *p = malloc(10);
실제 메모리에 쓰려고(write) 할 때, 남아 있는 메모리가 없는 경우가 문제가 되는데, C 언어 semantic에는 recourse가 없으므로, 이 경우, 대개 OS가 해당 프로그램을 죽이게(kill) 됩니다. (당연히, malloc은 메모리가 없을 경우, 널 포인터를 리턴하게 되므로, malloc의 리턴 값을 확인했다면, 할당한 메모리가 아닌 다른 메모리에 접근할 까닭이 없습니다.)
이와 같이 “lazy allocation” 방식을 쓰는 시스템에서는 대개 메모리가 부족하다는 것을 알려주는 시그널을 제공합니다. 그러나 프로그램에서 이 시그널을 처리하려면 이식성이 떨어지는 프로그램이 될 가능성이 높습니다. 이 방식을 제공하는 시스템 중에는 사용자 또는 프로세스 단위로 이 기능을 끄는(off) 기능을 제공하는 (즉, 전형적인 malloc semantic을 쓰는) 것도 있습니다만, 시스템마다 매우 달라집니다.
size_t
이며, 아마 unsigned long
타입일 가능성이 높습니다. 만약에 int
타입(또는 unsigned int)을
전달했다면, malloc이 (깨진) 쓰레기 값을 받았을 가능성이 있습니다.
(또는 long 타입을 전달했는데 size_t
가 int일 경우에도
이럴 가능성이 있습니다.)
일반적으로, 표준 라이브러리 함수의 선언을 extern으로 직접 써 주는 것보다 알맞는 헤더 파일을 포함시키는 것이 훨씬 더 안전합니다. 덧붙여 질문 7.16도 참고하시기 바랍니다.
비슷하면서 잘 알려진 또 다른 문제는, size_t
타입의 값을
(sizeof의 값 포함) printf의 %d를 써서 출력하는
것입니다. 이식성이 높은 코드를 얻기 위해서, 주어진 값을 직접
(unsigned long으로) 캐스팅한 다음 %lu 포맷으로 출력하는 것이
좋습니다.
printf("%lu\n", (unsigned long)sizeof(int));덧붙여 질문 15.3도 참고하시기 바랍니다..
size_t
를 쉽게 출력하기 위해, printf,
scanf 함수들에 z modifier를 추가했습니다.
질문
13.1을 참고하기 바랍니다.
double *array = malloc(256 * 256 * sizeof(double));이 때 malloc()은 널 포인터를 리턴하지 않았지만 프로그램이 매우 이상하게 동작합니다. 제 생각에는 메모리를 겹쳐 쓰는(overwrite) 현상이 발생한 것 같거나 malloc()이 원하는 만큼의 큰 메모리를 할당한 것 같지가 않습니다.
size_t
타입이 (malloc()이 인자로 받는
타입) 32 비트라면, 그리고 int
가 16 비트라면,
위 코드에서 malloc의 인자로 256 * (256 * sizeof(double))를
써서 해결할 가능성도 있습니다. (질문
3.14 참고).
그렇지 않다면 여러분은 이 메모리를 작은 크기로 쪼개어 쓰거나,
또는 32 비트 컴퓨터나 컴파일러를 쓰거나, 비표준으로 제공되는
메모리 할당 함수를 써야 할 것입니다. 덧붙여 질문
7.15,
19.23도 참고하시기 바랍니다.
예를 들어, 리스트의 노드가 다음과 같을 경우:
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를 부르지 않고, 필요없는 모든
node를 grave 포인터에 리스트로 보관하고, 필요한 경우,
(즉 new_node()
를 불렀을 경우) 리스트에 보관된 노드를 다시
씁니다. 최적화를 위해서, 일정한 갯수만 grave 리스트에 보관하고,
나머지는 free를 불러서 해제하는 방식으로도 만들 수 있습니다.
이런 “object caching”이 모든 문제를 해결해 주지는 않습니다. 상황에 따라, 이 방식이 도움이 될 경우가 있으므로, 여기에서 소개한 것이지, 항상 좋다는 것은 아니라는 것을 기억하시기 바랍니다.
strlen(s) + 1
을
쓰지 않고 malloc(strlen(s))를 쓰는 경우가 흔합니다.7.2 다른 문제로 이미 free()를 불러서 반환한 메모리 블럭에
쓰려고(write) 하거나 (질문
7.30 참고),
free()를 같은 포인터에 대해 두 번 호출하는 경우도
있습니다. (이 중 몇가지는 표준에 의해 인정되고 있습니다. ANSI
호환 시스템에서 크기가 0인 메모리 블럭을 malloc, realloc
등으로
할당하거나, 널 포인터를 free()로 반환하는 것은 아무런 문제도
일으키지 않습니다. 물론 오래된 시스템에서는 문제가 됩니다.)
대개 이러한 실수는 금방 발견되지 않고, 오랫동안 숨어 있거나,
또는 전혀 상관없는 코드 부분에서 에러가 발생한 것처럼 보이기도 해서
수정하기가 매우 까다롭습니다.
대부분 malloc implementation은 중요한 정보를 할당한 메모리 바로 옆에 보관하기 때문에, 사용자가 포인터를 조금이라도 잘못 쓰게 되면, 심각한 문제가 발생합니다.
대부분의 프로그래머들이 반환된 메모리의 내용을 일부러 다시 쓰지는 않습니다만, 실수로 반환한 메모리 공간을 쓰기도 합니다. 다음에 나온 singly-linked 리스트를 반환하는 코드를 보시기 바랍니다:
struct list *listp, *nextp; for (listp = base; listp != NULL; listp = nextp) { nextp = listp->next; free(listp); }만약 위 코드가 임시 변수 nextp를 쓰지 않고,
listp = listp->next
를 썼다면, 이미 반환한 메모리를
listp->next
로 다시 접근하려 한다는 것을 알 수 있습니다.
일단 해제된 포인터 값은 엄밀히 말해서, 유효하지 않습니다(invalid). 그리고 (dereference가 아니더라도) 어떠한 목적으로 (심지어, 단순히 대입하거나 비교하는 것도) 이 값을 쓰는 것은, 물론 구현 방법에 따라 다르긴 하지만, 이론상 문제를 발생할 수 있습니다. (물론, 대부분의 시스템이 문제가 없어 보이는 invalid 포인터 쓰임새를 너그럽게 봐 주지만, 표준은 확실하게, 어떤 것도 보장될 수 없다고 말합니다. 또한 어떤 시스템은 구조상 이런 exception 상황이 흔히 발생합니다.)
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도 참고하시기 바랍니다..
realloc(..., 0)
처럼
써서 메모리를 해제하는 것) 괜찮다고 말합니다. 그러나 대부분의
오래된 구현 방법에서는 이러한 사용법을 제공하지 않습니다.
따라서 표준이기는 하지만 완전히 호환성을 갖춘 방법이 아닙니다.
realloc의 첫 인자로 널 포인터를 전달하면 메모리를
증가적으로 동적으로 할당하는 알고리즘을 (self-starting incremental
allocation algorithm) 만들기 쉽습니다.
p = malloc(m * n); memset(p, 0, m * n);0으로 채운다는 것은 할당한 메모리의 모든 비트를 0으로 채운다는 뜻입니다. 따라서 이 값이 널 포인터가 아닐 수도 있으며(여기에 관한 것은 5 절을 참고하기 바랍니다.) 실수(floating-point)로 0이 아닐 수도 있습니다. 그리고 calloc()으로 할당한 메모리를 해제할 때에도 free()를 씁니다.
alloca() 함수는 이식성있게 만들 수가 없습니다. 그리고 일반적인 스택을 사용하지 않는 컴퓨터에서는 매우 만들기 어려운 함수입니다. 특히 alloca로 할당한 메모리를 리턴하는 경우, 심각한 문제가 발생할 수 있습니다. 예를 들면:
fgets(alloca(100), 100, stdin);
이런 이유에서 alloca 함수는 표준 함수가 아니며, 높은 이식성이 요구되는 프로그램에서는 (매우 쓸모 있기는 하지만) 쓸 수 없습니다.
Seong-Kook Shin