프로그래밍 언어에서 존재 하지 않는다는 것을 표현하기 위해
null이라는 표현이 사용되며, 언어에 따라 null, none등으로 표현합니다.
C++에는 없음을 표현하기 위해 NULL과 nullptr을 사용할 수 있는데,
왜 두개가 따로 존재하는걸까요?
nullptr과 NULL
nullptr은 C++ 11에 추가된 키워드입니다.
단어 그대로, null pointer를 나타냅니다.
하지만,C++ 11 전 버전들에서도 Null Pointer을 당연히 표현할 수 있었을 것이고,
이를 표현하는 방법은 nullptr만 있는 것은 아니고,
0 또는 NULL을 이용할 수 있습니다.
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
와 같이 C++에서는 NULL을 0으로 정의한 것을 알 수 있습니다
nullptr이 추가된 이유
그러면 왜 nullptr은 새로 추가 된 것일까요?
결론부터 말하면 모호성을 줄이고 가독성, 안정성을 높이기 위해
도입 되었습니다.
아래 코드를 실행하면
int main() {
int* pt1 = 0;
int* pt2 = nullptr;
cout << (pt1 == pt2);
}
1이 출력됩니다.
즉, 0(또는 NULL)과 nullptr을 포인터 변수의 값으로 사용하면 동일함을 알 수 있습니다 (둘다 빈 포인터를 의미)
함수에서의 nullptr vs NULL
아래 코드를 실행하면
void func(int* pt) {
cout << "int pointer\n";
}
void func(int num) {
cout << "int num\n";
}
int main() {
func(NULL);
func(nullptr);
}
출력
int num
int pointer
NULL은 상수 0이고, nullptr은 null pointer이므로 오버로딩 된 함수의
인자의 우선순위는 위와 같이 동작합니다.
NULL은 0이므로, int타입 인자로 받는 func함수를 호출합니다.
void func(int num) {
cout << "int num\n";
}
int main() {
func(NULL);
func(nullptr); // 컴파일 오류
}
하지만, nullptr은 0이 될 수 없습니다.
0은 Null Pointer을 표현할 수 있지만, nullptr이 0을 표현할 수 없습니다.
즉, nullptr은 integral type conversion이 안되는 중요한 특성이 있습니다.
이를 통해 안정성을 확보할 수 있죠.
하지만 0, NULL 타입은 포인터 타입으로 conversion이 가능하며
따라서
void func(int* pt) {
cout << "int pointer\n";
}
int main() {
func(NULL);
func(nullptr);
}
은 정상적으로 컴파일 되며 둘다 func(int* pt)를 호출합니다.
nullptr은 타입이 아니고, nullptr은 "상수"입니다.
void func(int* pt) {
cout << "int*\n";
}
void func(char* pt) {
cout << "char*\n";
}
int main() {
func(nullptr); // 컴파일 오류
char* ch = nullptr;
func(ch); // char*를 인자로 받는 func()가 호출됨
func((char*)nullptr); // 컴파일 문제 X. char*를 인자로 받는 func()가 호출됨
}
여기서 func(nullptr)은 두개의 func() 중에 어느 함수를 호출할지
결정할 수 없습니다.
즉, 사실은 nullptr자체로는 포인터 타입이 아니며
단지 pointer타입으로 conversion이 가능한 특성을 가집니다.
방금 예시에서 func(nullptr)은 어느 타입으로 conversion을 할 지
결정할 수 없기 때문에 포인터 타입 여러개로 오버로딩된 함수가 있으면
컴파일이 불가능하지만,
아래와 같이 nullptr의 자체 타입인 nullptr_t를 타입으로 받는
오버로딩된 함수가 있으면 최우선으로 동작하기 때문에 컴파일에 오류가 없습니다.
void func(int* pt) {
cout << "int*\n";
}
void func(char* pt) {
cout << "char*\n";
}
void func(nullptr_t pt) {
cout << "nullptr_t\n";
}
int main() {
func(nullptr);
}
-> 이제는 컴파일 오류 없이, "nullptr_t"타입으로 인자를 받는 오버로딩 된 함수가
호출됩니다.
비주얼스튜디오 기본 컴파일러로
cout << typeid(nullptr).name() << "\n";
cout << typeid((char*)nullptr).name() << "\n";
출력
std::nullptr_t
char * __ptr64
nullptr은 아까 언급했듯이 타입이 아니라 상수이고,
이 nullptr을 표현하기 위한 타입은 nullptr_t입니다.
하지만 보통 nullptr은 다른 포인터 타입에 대입해서 사용하므로
nullptr_t가 직접 사용되는 일은 거의 없습니다.
결론
Null Pointer을 표현할 때는 0 또는 NULL 또는 nullptr로 표현할 수 있지만,
안정성을 위해서는 nullptr을 사용하는 것이 가장 좋습니다.
또한 실수를 방지하기 위해, 어떤 함수를 integral타입과 포인터 타입으로
모두 오버로딩 하는 행위 역시 자제해야합니다.