[C++] NULL vs nullptr

hwhyeons·2024년 8월 20일

프로그래밍 언어에서 존재 하지 않는다는 것을 표현하기 위해
null이라는 표현이 사용되며, 언어에 따라 null, none등으로 표현합니다.

C++에는 없음을 표현하기 위해 NULLnullptr을 사용할 수 있는데,

왜 두개가 따로 존재하는걸까요?



nullptr과 NULL

nullptr은 C++ 11에 추가된 키워드입니다.

단어 그대로, null pointer를 나타냅니다.

하지만,C++ 11 전 버전들에서도 Null Pointer을 당연히 표현할 수 있었을 것이고,
이를 표현하는 방법은 nullptr만 있는 것은 아니고,
0 또는 NULL을 이용할 수 있습니다.


참고로 0과 NULL은 동일합니다. 실제로 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타입과 포인터 타입으로
모두 오버로딩 하는 행위 역시 자제해야합니다.

0개의 댓글