Chapter 17 - Advanced Uses of Pointers - 2/3에서 계속됨.
Q&A
Q: NULL 매크로는 무엇을 나타내는가?
A: NULL은 0을 나타낸다. 만약 문맥상 포인터가 올 곳에 0을 넣으면, 컴파일러는 그것을 정수 0이 아닌 포인터로 간주한다. NULL 매크로는 단지 혼란 방지를 위해 제공된다.
p=NULL; 은 p가 포인터라는 것을 확실하게 나타내 준다.
Q: 0이 널 포인터를 나타내기 위해 사용된다면, 널 포인터는 모든 비트값이 0인 주소인가?
A: 꼭 그렇지는 않다. 컴파일러마다 다른 방식으로 널 포인터를 표현할 수 있고, 모든 컴파일러가 0 주소를 사용하지는 않는다. 어떤 컴파일러는 존재하지 않는 메모리 주소를 널 포인터에 사용한다. 그래서 널 포인터를 통해 메모리에게 접근하려고 하면 하드웨어에 의해 감지된다.
널 포인터가 컴퓨터에 어떻게 저장되는지는 우리의 걱정거리는 아니다; 컴파일러가 걱정해야 할 디테일이다. 중요한 것은 포인터의 문맥에서 0이 사용되면 컴파일러가 적절한 내부 형태로 알아서 변환해 준다는 것이다.
Q: NULL을 null 문자로 사용해도 괜찮은가?
A: 절대 그렇지 않다. NULL은 널 포인터를 나타내는 매크로지, null character를 나타내지 않는다. 어떤 컴파일러에서는 NULL을 null character에서 사용해도 괜찮지만, 전부다 괜찮지는 않다(어떤 컴파일러는 NULL을 (void *) 0으로 정의하기 때문에). 어떤 경우에도 NULL을 포인터가 아닌 다른 것으로 사용하는 것은 굉장한 혼란을 불러일으킨다. 만약 null character에 이름을 붙이고 싶다면
#define NUL '\0'
Q: malloc이나 다른 메모리 할당 함수의 리턴값에 캐스팅하는 것의 장점이 있는가?
A: 보통 그렇지 않다. 이 함수들이 리턴하는 void * 포인터에 캐스팅하는 것은 불필요하다. 왜냐하면 void * 형식의 포인터는 assign되는 포인터의 형식으로 자동 변환되기 때문이다. 리턴 값에 캐스팅하는 습관은 옛 버전의 C에서 내려온 잔재이다(메모리 할당 함수가 char * 값을 리턴했기 때문에 형변환을 해야만 했었다). C++ 코드로 컴파일할 목적으로 디자인된 코드에서는 형변환이 이득이 될 수 있지만, 그것이 유일한 이유이다.
Q: calloc 함수는 메모리 블록의 모든 비트를 0으로 초기화한다. 이것은 블록의 모든 데이터 아이템이 0이 된다는 것을 의미하는가?
A: 보통은 그렇지만 항상 그렇지는 않다. 모든 비트를 0으로 설정하면 정수는 0이 된다. 모든 비트를 0으로 설정했을 때 부동 소수점 형식의 수는 보통 0이 되지만, 항상 그렇다는 보장은 없다 - 부동소수점 숫자가 저장되는 방식에 따라 다르다. 포인터에 대해서도 마찬가지로, 모든 비트가 0인 포인터가 반드시 null 포인터가 되지는 않는다.
Q: malloc을 잘못된 인수로 호출해서 너무 많은 메모리나 너무 적은 메모리를 할당하는 일이 꽤 흔한 실수 같다. 더 안전하게 malloc을 사용할 방법이 있는가?
A: 그렇다. 어떤 프로그래머들은 malloc으로 하나의 object를 위한 메모리를 할당할 때 다음과 같은 관용구를 사용한다:
p = malloc(sizeof(*p));
sizeof(*p)는 p가 가리키는 object의 크기이므로, 이 문장은 할당될 정확한 분량의 메모리를 할당하는 것을 보증한다. 언뜻 보기에 이 관용구는 좀 이상해 보인다. p가 초기화되어있지 않다면 *p의 값은 정의되어 있지 않기 때문이다. 그러나 sizeof 연산자는 *p의 값을 측정하는 것이 아니라 단지 크기만을 계산하기 때문에, 이 관용구는 p가 초기화되어있지 않거나 null 포인터를 담고 있더라도 작동한다. 만약 n개의 원소를 가진 배열의 메모리를 할당하려면, 다음과 같이 수정해서 사용한다.
p = malloc(n * sizeof(*p));
위 idiom을 사용하는 예제를 만들어 보았다.
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
int main(void) {
int i, n;
printf("문자열 변수의 크기를 입력하세요: ");
scanf("%d", &n);
char *str = malloc(n * sizeof(*str));
printf("문자 %d개를 입력하세요: ", n - 1);
getchar();
for (i = 0; i < n; i++)
scanf("%c", str + i);
*(str + n - 1) = '\0';
printf("입력받은 문자열: %s\n", str);
printf("str에 저장된 문자들 (아스키 코드): ");
for(i=0; i<n; i++)
printf("%d ", *(str+i));
return 0;
}
Q: qsort는 왜 sort가 아니고 qsort인가?
A: qsort라는 이름은 C.A.R. Hoare의 Quicksort algorithm(1962년 출판)에서 따온 것이다. 역설적이게도, 많은 컴파일러들이 qsort에서 퀵소트 알고리즘을 사용하지만 C 표준은 qsort가 꼭 퀵소트 알고리즘을 쓰도록 강제하지는 않는다.
Q: qsort를 호출할 때 첫번째 인자를 void* 형으로 형변환 시켜야 하는가?
A: 그렇지 않다. 모든 형식의 포인터는 자동으로 void* 형식으로 변환된다.
Q: 문자열의 배열을 정렬할 때, qsort의 비교 함수로 strcmp를 사용하고 싶다면?
int compare_strings(const void *p, const void *q)
{
return strcmp(p, q);
}
이렇게 넣었을 때 컴파일은 되지만 정렬이 되지 않는다.
A: strcmp 자체를 qsort에 전달할 수는 없다. qsort에 들어가야 할 비교 함수는 두개의 cost void * 파라미터를 가져야 하기 때문이다. 이 compare_strings 함수는 작동하지 않는다. 왜냐하면 p와 q가 문자열(char * 포인터)이라고 잘못 가정하기 때문이다. 사실 p와 q는 char * 포인터를 담은 배열의 원소를 가리키는 포인터이다. compare_strings를 고치려면, p와 q를 char**로 형변환하고, * 연산자로 하나의 참조 단계를 제거해야만 한다.
int compare_strings(const void *p, const void *q)
{
return strcmp(*(char **)p, *(char **)q);
}