C++11부터 도입된 nullptr는 포인터 리터럴(pointer literal)을 나타내는 키워드입니다. nullptr는 std::nullptr_t 타입의 prvalue이며, 모든 포인터 타입 및 멤버 포인터(pointer to member) 타입의 널 포인터 값으로 암시적으로 변환될 수 있는 특징을 가지고 있습니다.
이번 글에서는 예제 코드를 통해 nullptr가 어떻게 동작하는지, 그리고 기존의 NULL과 어떤 차이가 있는지 자세히 살펴보겠습니다.
NULL 사용 시 발생하는 문제
먼저 함수 오버로딩 상황에서 NULL을 사용하면 어떤 문제가 발생하는지 확인해 보겠습니다.
#include<iostream>
using namespace std;
int my_func(int N){ // int 타입 매개변수를 가진 함수
cout << "Calling function my_func(int)";
}
int my_func(char* str) { // char* 타입 매개변수를 가진 오버로딩된 함수
cout << "calling function my_func(char *)";
}
int main() {
my_func(NULL); // my_func(char *) 호출을 의도했지만 컴파일 에러 발생
}출력 결과
[Error] call of overloaded 'my_func(NULL)' is ambiguous [Note] candidates are: [Note] int my_func(int) [Note] int my_func(char*)
왜 모호성(ambiguous) 오류가 발생할까?
위 프로그램의 문제 원인은 NULL의 정의에 있습니다. 전통적으로 NULL은 (void*)0으로 정의되며, 정수형 타입으로도 변환이 허용됩니다. 그 결과 컴파일러 입장에서는 my_func(NULL) 호출이 my_func(int)인지 my_func(char*)인지 판단할 수 없어 모호성 오류가 발생하게 됩니다.
nullptr로 해결하기
이제 NULL 대신 nullptr를 사용하면 어떻게 되는지 확인해 보겠습니다.
#include<iostream>
using namespace std;
int my_func(int N){ // int 타입 매개변수를 가진 함수
cout << "Calling function my_func(int)";
}
int my_func(char* str) { // char* 타입 매개변수를 가진 오버로딩된 함수
cout << "calling function my_func(char *)";
}
int main() {
my_func(nullptr); // my_func(char *)가 정상적으로 호출됨
}출력 결과
calling function my_func(char *)
컴파일러가 의도대로 char* 버전의 함수를 정확하게 선택한 것을 볼 수 있습니다. nullptr는 순수한 포인터 리터럴이기 때문에 포인터 타입 오버로드와 정수 타입 오버로드 사이의 모호함이 발생하지 않습니다.
nullptr와 NULL의 핵심 차이점 정리
nullptr는 기존에 NULL이 사용되던 모든 곳에서 대체하여 사용할 수 있습니다. NULL과 마찬가지로 어떤 포인터 타입으로든 변환이 가능하지만, 결정적인 차이점이 하나 있습니다. 바로 nullptr는 NULL과 달리 정수형(integral type)으로 암시적으로 변환되지 않는다는 점입니다.
이러한 특성 덕분에 nullptr를 사용하면 포인터와 정수가 혼용될 수 있던 코드에서 발생하는 잠재적인 버그와 모호성 문제를 컴파일 타임에 방지할 수 있습니다. 따라서 현대 C++ 코드에서는 널 포인터를 표현할 때 반드시 NULL이 아닌 nullptr를 사용하는 것이 권장됩니다.