For each pointer type, C defines a special pointer value, the null pointer, that is guaranteed not to point to any object or function of that type. (The null pointer is analogous to the nil pointer in Pascal and LISP.) C programmers are often confused about the proper use of null pointers and about their internal representation (even though the internal representation should not matter to most programmers). The null pointer constant used for representing null pointers in source code involves the integer 0, and many machines represent null pointers internally as a word with all bits zero, but the second fact is not guaranteed by the language.
Because confusion about null pointers is so common, this chapter discusses them rather exhaustively. (Question 5.13- 5.17 are a retrospective on the confusion itself.) If you are fortunate enough not to share the many misunderstandings covered or find the discussion too exhausting, you can skip to question 5.15 for a quick summary.
&
연산자는 절대로 널 포인터를 만들어 낼
수 없으며, 실패하지 않는 한 malloc() 함수도 널 포인터를
리턴하지 않습니다 (malloc()은 실패할 경우, 널 포인터를
리턴합니다. 그리고 이 것이 널 포인터의 쓰임새 -- “할당되지 않은”
또는 “어떠한 것도 가리키지 않는”을 의미하는 특별한 포인터로
쓰이는 것 -- 중 하나입니다.)
널 포인터와 초기화되지 않은 포인터5.1와는 개념상 완전히 다릅니다. 널 포인터는 어떠한 오브젝트나 함수도 가리키지 않는 포인터이고, 초기화되지 않는 포인터는 어떤 값을 가지는 지 모르므로, 아무 오브젝트나 가리킬 수 있는 포인터입니다. 질문 1.30, 7.1, 7.31을 참고하기 바랍니다.
위에서 설명한 것처럼, C 언어는 각각의 포인터 타입에 따라 널 포인터가 존재합니다. 그리고 널 포인터의 실제 값은 각 타입에 따라 서로 다를 수 있습니다. 컴파일러가 각 타입에 따른 실제 값으로 변경해 주기 때문에 프로그래머들은 각 타입에 따라 서로 다른 널 포인터의 내부적인 값을 알 필요가 전혀 없습니다 (질문 5.2, 5.5, 5.6을 참고).
char *p = 0; if (p != 0)덧붙여 질문 5.3도 참고하시기 바랍니다.
그러나, 함수의 인자로 포인터를 전달할 경우, 포인터가 쓰일 곳(pointer context)으로 인식하지 못하고, 단순히 정수 0으로 인식할 가능성이 있습니다. 이럴 때에는 널 포인터라는 것을 강제적으로 캐스팅을 써서 알려 주어야 합니다. 예를 들어, UNIX 시스템 콜인 execl은 가변 인자 리스트5.3를 받습니다. 이 함수는 인자의 끝을 알리기 위해서 널 포인터를 마지막으로 전달해야 합니다. 즉:
execl("/bin/sh", "sh", "-c", "date", (char *)0);
마지막 인자의 (char *)
캐스팅이 생략될 경우,
컴파일러는 이를 널 포인터로 인식하지 못하고 단순히 정수 0으로
인식합니다. (대부분의 UNIX 매뉴얼은 이 부분을 잘못 설명하고
있으니 주의해야 합니다. 덧붙여 질문
5.11도 참고하시기 바랍니다.)
함수의 프로토타입(prototype)이 있을 경우, 인자 전달은 대입(assignment) 연산으로 인식되기 때문에, 캐스팅을 할 필요가 없습니다. 왜냐하면 함수 프로토타입이 컴파일러에게 적절한 타입이 무엇이라는 것을 알려주기 때문입니다. 따라서 단순히 0만 전달해도, 컴파일러가 알아서 널 포인터로 바꾸어 줍니다. 그러나 가변 인자 리스트를 쓰는 함수의 인자는 프로토타입을 알더라도, 각각의 인자에 대한 타입을 알 수 없으므로 이런 함수의 인자로 쓰인 널 포인터에는 반드시 캐스팅을 써 주어야 합니다. (질문 15.3을 참고하시기 바랍니다.) varargs 함수에 쓰일 것을 대비하고, 함수 프로토타입이 없을 경우도 대비하고, ANSI 호환이 아닌 컴파일러에 쓰일 것을 대비하기 위해 널 포인터 상수 0에 항상 캐스팅을 하는 것이 혼동되지 않고 안전할 수 있습니다.
아래 표는 널 포인터 상수(0)를 그대로 써도 좋은 경우와, 그렇지 않는 경우에 대한 상황을 알려줍니다:
그냥 0을 써도 좋은 경우: | 캐스팅이 반드시 필요한 경우: |
초기화(initialization) | |
대입(assignment) | |
비교(comparison) | |
함수 호출,
프로토타입 있음 (prototype in scope)
|
함수 호출, 프로토타입 없음 (no prototype in scope) |
고정된 인자
|
함수 호출에서 가변 인자 (variable argument) 사용 |
if (p)
”라고 쓰는
것이 안전한가요? 만약 널 포인터의 실제 값이 0이 아닐 경우에는
어떻게 되는 건가요?
&&
,
||, ! 그리고 ?:와 같은 연산자에서), 거짓은 0을 의미하며,
참(true)은 0이 아닌 값을 의미하게 됩니다.
따라서 다음과 같이 쓰게 되면:
if (expr)실제로 `expr'이 무엇이든지, 컴파일러는 위의 코드를 다음의 코드와 같은 것으로 봅니다.
if ((expr) != 0)따라서 `expr'을 주어진 `p'로 바꾸면, `
if (p)
'가 `if (p != 0)
'이 됩니다.
그리고 이 수식은 비교를 하는 문맥(comparison context)이기 때문에,
컴파일러는 0이 널 포인터 상수라는 것을 알고, 실제 널 포인터
값으로 변경해줍니다. 크게 특별한 기술을 사용한 것도 아니고,
컴파일러는 두 경우 모두 같은 코드를 만들어 냅니다.
여기에서 실제 널 포인터의 값이 0인지 아닌지는 전혀 문제되지 않습니다.
다음과 같이 불리언 부정(not) 연산자인 !
를 쓰는 것은:
!expr
다음과 같이 쓰는 것과 완전히 같습니다:
(expr) ? 0 : 1
또는 다음과 같이 쓸 수 있습니다:
((expr) == 0)
따라서, 다음과 같이 쓰는 것은:
if (!p)
다음과 같이 쓰는 것과 같습니다:
if (p == 0)줄여서 (abbreviation)
if (p)
로 쓰는 것은 전혀 문제될 것이
없습니다. 그러나 어떤 사람들은 이런 식으로 코딩하는 것이 나쁜
습관이라고 말합니다 (물론 어떤 사람들은 좋은 습관이라고 말합니다.
질문
17.10을 참고하기 바랍니다).
#define
) 있나요?
<stdio.h>
와 <stddef.h>
를 포함한 여러 헤더
파일에 정의되어 있으며, 실제로 0으로 정의되어 있으며, 대개는
(void *)로 캐스팅되어 있습니다 (질문
5.6 참고).
따라서 정수 0과 널 포인터 상수인 0을 쉽게 구별하기 위해, 널
포인터가 오는 곳에 NULL을 사용합니다.
NULL을 쓰는 것은 단순히 스타일적인 문제입니다; 전처리기가 NULL을 0으로 바꾸어주므로, 컴파일러가 볼 때에는 모두 0으로 보게 됩니다. 따라서 함수 인자로 사용할 경우에는 0을 사용할 때와 마찬가지로 NULL도 캐스팅을 해줘야 할 필요가 있습니다.
질문 5.2의 표에서 0 대신에 NULL을 그대로 쓸 수 있습니다 (캐스팅하지 않는 NULL은 캐스팅하지 않는 0과 같기 때문입니다).
그러나 NULL은 반드시 포인터가 쓰이는 문맥에서만 쓰여야 합니다. 질문 5.9를 참고하기 바랍니다.
프로그래머가 널 포인터를 쓸 경우, 0을 쓰던지 NULL을 쓰던지에 상관없이, 컴파일러가 실제 컴퓨터의 내부적인 널 포인터 값으로 만들어 줍니다. (다시 말하지만, 컴파일러는 0이 포인터가 쓰일 곳에 쓰인 경우, 알아서 널 포인터로 바꾸어 줍니다. 질문 5.2 참고) 그렇기 때문에, 실제 널 포인터가 0이 아닌 다른 값을 갖는 시스템에서 NULL을 0으로 정의한 것은 당연합니다. 컴파일러는 0이 포인터가 쓰일 곳에 쓰인 경우, 항상 그 시스템에 맞는, 올바른 널 포인터 값을 만들어 내야 합니다. 상수 0은 널 포인터 상수이며, NULL은 단순히 같은 것을 의미하는 또다른 이름일 뿐입니다. (질문 5.13 참고)
C 표준 4.1.5장을 보면, NULL에 대해서 “expands to an implementation-defined null pointer constant,”라고 표현한 문장이 있습니다. 즉, 어떤 형태의 0을 쓰던지, void * 캐스트를 쓸 것인지는 컴파일러가 결정합니다; 질문 5.6, 5.7 참고. 여기에서 “implementation-defined”란 용어가 NULL이 0이 아닌, 내부적으로 쓰이는 널 포인터 값으로 쓰인다는 것을 뜻하지는 않습니다.
#define NULL ((char *)0)
함수 인자로 NULL을 전달할 때, 캐스팅하지 않아도 되지 않을까요?
그러나 ANSI C는 NULL을 다음과 같이 정의하는 것을 허락하고 있습니다5.4:
#define NULL ((void *)0)
그러나 NULL을 위와 같이 정의하는 것은 NULL을 잘못쓰는 문제를 어느 정도 (모든 포인터가 같은 내부 표현 방식을 가진 경우에만) 해결해 줄 수 있습니다. (ASCII NUL 문자가 필요한 경우, 질문 5.9 참고) 덧붙여 질문 5.7도 참고하시기 바랍니다.
최근에 나온 “flat” 메모리 구조를 가진 시스템에 익숙해져 있는 프로그래머라면 이러한 “타입에 따라 서로 표현 방식이 다른 포인터”라는 개념이 낯설 것입니다. 질문 5.17을 참고하기 바랍니다.
포인터가 쓰일 곳에서만, NULL과 0이 같다는 것을 잊어서는 안됩니다.
포인터가 쓰이지 않는 곳에서 NULL을 쓰는 것은, 만약 제대로
동작한다 할지라도, 쓰면 안됩니다.
왜냐하면 그럴 경우, 잘못된 스타일에 관한
메시지가 발생하기 때문입니다. (게다가 ANSI는 NULL을
((void *)0)
으로 정의할 수 있도록 하고 있으므로), 포인터가
쓰일 수 없는 곳에 NULL을 쓸 수 없는 시스템도 있습니다.
특히 ASCII null 문자 (NUL)이 쓰일 곳에 NULL을 쓰면 안됩니다.
꼭 매크로를 써야 한다면 다음과 같이 따로 정의해서 쓰는 것이
더 낫습니다:
#define NUL '\0'
execl("/bin/sh", "sh", "-c", "date", NULL); /* WRONG */
NULL을 ((void *)0)으로 정의하고 있는 (질문 5.6 참고) 컴파일러에서 이 코드는 동작합니다5.5. 그러나, 포인터와 정수가 다른 크기나 표현 방식을 쓰는 시스템이라면, 아래와 같은 코드는 (똑같이 잘못된 것이면서) 동작하지 않을 수 있습니다:
execl("/bin/sh", "sh", "-c", "date", 0); /* WRONG */
이식성이 뛰어난, 좋은 코드는 다음과 같습니다:
execl("/bin/sh", "sh", "-c", "date", (char *)NULL);
캐스트를 써서, 위 코드는, 시스템에서 포인터와 정수가 크기가 다르거나, 내부 표현 방식이 다르더라도 동작하며, NULL의 정의가 어떤 식으로 되어 있느냐에 상관없이 동작합니다. (질문 5.2에서 NULL 대신에 0을 쓴 코드는 같은 이유로 올바른 코드입니다; 덧붙여 질문 5.9도 참고하시기 바랍니다.)
#define Nullptr(type) (type *)0이게 좋은 습관일까요?
#define
)되어 있습니다.
(질문
5.4 참고).
""
)을 나타내는
다른 말로, C 언어에서 `널 문자열'이라는 용어를 쓰는 것은
혼동을 가져옵니다. 비어있는 문자열은 널 문자('\0
')를
말하는 것이지, 널 포인터와는 상관 없습니다.
이 글에서는 “널 포인터(null pointer)”라는 용어를 위의 1번의 목적으로 사용합니다. 3 번의 목적으로는 0이나 “널 포인터 상수”라는 표현을 쓰며, 4 번의 목적으로 “NULL”을 사용합니다5.7.
if (p == 0)
”에서,
사실은 0을 널 포인터로 생각하고 비교하는 것이지만 p를 정수형으로
바꾸고 비교하는 것처럼 보일 수도 있습니다.
마지막으로 여러 의미를 가지는
(질문
5.13 참고) “null”이라는 용어를 건성으로 보는 경향이 있습니다.
C 언어에서 이런 혼동을 없애기 위한 방법으로, 널 포인터 용으로 (Pascal의nil과 같은) 키워드(keyword)를 만들어 썼다면 좋은 효과를 얻었을 것이라고 생각합니다. 그러면 컴파일러는 “nil”을 적절한 널 포인터로 바꾸어 줄 수 있을 것이며, 널 포인터가 올 수 없는 곳에서는 경고를 만들어 낼 수 있을 것입니다. 그러나 현재 C 언어에서 널 포인터를 나타내는 키워드는 “nil”이 아니라 “0”입니다. 그리고 널 포인터가 올 수 없는 곳에 0이 쓰이면, 에러가 발생하는 것이 아니라 정수 0으로 해석되며, 널 포인터가 와야 할 자리에 캐스팅하지 않는 0이 오게 되면, 동작하지 않을 수도 있다는 것이, 우리의 이상과는 다릅니다.
게다가 널 포인터에 대해 잘 이해하기 위해, 실제로 내부적으로 표현되는 널 포인터의 값을 (0인지 아닌지에 대해) 알 필요가 전혀 없습니다. 단순히 널 포인터가 내부적으로 0으로 표현된다고 생각한다고 해서, 코드를 작성하기 쉬워지는 것도 아닙니다. (잘못된 calloc()에 대한 설명을 질문 7.31에서 참고하기 바랍니다.)
그리고 널 포인터가 0이라고 해도 포인터의 크기가 타입에 따라 달라질 수 있기 때문에 여전히 함수 호출에서 캐스팅을 해야 합니다. (만약 질문 5.14에서 말한 것처럼 “nil”이 널 포인터로 쓰일 수 있다면 널 포인터가 0인지 아닌지에 대한 논쟁 자체가 의미없는 것이 될 것입니다.)
char *
)보다 워드 포인터 (int *
)가
크기가 더 작습니다.
Data General사의 Eclipse MV 시리즈는 기계 수준에서 세 가지의
포인터 타입을 제공합니다 (워드, 바이트, 비트 포인터). C 언어에서는
두 가지 형태를 사용하며 char *
와 void *
는
바이트 포인터로, 나머지 포인터는 워드 포인터로 구현됩니다.
어떤 Honeywell-Bell 메인프레임에서는 널 포인터 값으로 06000을 사용합니다.
CDC Cyber 180 시리즈는 링(ring), 세그먼트, 옵셋 부분으로 이루어진 48 비트 포인터를 사용하며, (링 11의) 대부분의 사용자는 널 포인터로 0xB00000000000를 사용합니다. 오래된 CDC는 “1의 보수(one's complement)” 방식을 사용하며, 잘못된 주소를 포함한 모든 데이터의 예외 상황에 모든 비트가 1인 수치를 사용합니다.
오래된 HP3000 시리즈는 위에서 소개한 다른 시스템처럼, char *
,
void *
타입에 대한 포인터와 나머지 포인터들을 바이트 어드레싱과
워드 어드레싱을 써서 구현하며, 두 어드레싱이 서로 다른 방식을
사용합니다.
Symbolics Lisp 컴퓨터에서는, (tagged architecture), 아예 수치로
표현되는 포인터를 제공하지 않습니다. C 널 포인터는 <NIL, 0>
으로
구현됩니다. (기본적으로 <object, offset>
을 사용함.)
8086 계열의 프로세서 (PC 호환) 에서는 `메모리 모델'에 따라 16 비트 데이터 포인터와 32 비트 함수 포인터를 쓸 수 있습니다. 또는 32 비트 데이터 포인터와 16 비트 함수 포인터를 쓸 수 있습니다.
어떤 64 비트 Cray 컴퓨터에서는 int *
를 한 워드의 하위 48
비트로 표현하며, char *
는 나머지 상위 16 비트를 옵셋으로 써서
표현합니다.
union { int *u_p; int u_i; /* assumes sizeof(int) >= sizeof(int *) */ } p; p.u_i = 0;
memset((void *)&p, 0, sizeof(p));
extern int location0;어셈블리나 링커의 특별한 명령을 써서 이 심볼이 주소 0을 가리키도록 합니다.
어떤 디버거들은 데이터 와치포인트(watchpoint)를 주소 0에 설정할 수 있도록 해 줍니다. 또는 아예 주소 0 근처의 약 20 바이트 정도를 다른 곳에 복사해두고 주기적으로 비교해서 변경되었는지를 검사할 수도 있습니다. 질문 16.8을 참고하기 바랍니다.
Seong-Kook Shin