The Legacy Approach: NULL in C++98/03
To ensure program stability and prevent undefined behavior associated with dangling pointers, it is standard practice to initialize pointers immediately upon declaration. If the target address is not yet known, the pointer should be set to an empty state. In the C++98 and C++03 standards, developers typically used one of two methods to achieve this:
int* dataPtr = 0;
int* dataPtr = NULL;
Under the hood in C++, NULL is a macro definition. While C treats NULL as (void *)0, C++ defines it strictly as the integer literal 0:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
This design choice stems from C++'s stricter type system. Unlike C, C++ does not allow an implicit conversion of void* to other pointer types (e.g., char* or int*). However, an integer can be implicitly converted to a pointer. To maintain compatibility with C and allow NULL to work as a generic initializer for various pointer types, the standard relies on the memory address 0x00000000, which is typically read-only.
Unfortunately, defining NULL as the integer 0 introduces significant ambiguity in specific scenarios, particularly during function overloading. The compiler cannot distinguish between an argument intended to be a null pointer and an integer zero.
#include <iostream>
using namespace std;
// Overloaded function for integer
void displayValue(int num) {
cout << "Integer version called: " << num << endl;
}
// Overloaded function for character pointer
void displayValue(char* str) {
cout << "Pointer version called" << endl;
}
// Overloaded function for single character
void displayValue(char c) {
cout << "Character version called" << endl;
}
int main() {
displayValue(NULL);
displayValue(0);
return 0;
}
When compiling the code above, both displayValue(NULL) and displayValue(0) resolve to the displayValue(int) overload. This demonstrates that within the C++98/03 context, NULL and the integer 0 are functionally identical, which prevents developers from reliably passing a null pointer to overloaded functions expecting a pointer type.
The Modern Solution: C++11 nullptr
To address these type-safety issues, C++11 introduced a new keyword: nullptr. This keyword is designed specifically to represent a null pointer constant. It can be used to initialize any pointer type, regardless of the data type it points to.
int main() {
int* intPtr = nullptr;
double* floatPtr = nullptr;
char* charPtr = nullptr;
void* voidPtr = nullptr;
return 0;
}
The keyword nullptr is of type std::nullptr_t. It implicitly converts to any pointer type, allowing the compiler to correctly match the type in the code above. Crucially, nullptr does not convert to integral types like int, which resolves the ambiguity presant with NULL.
Using nullptr effectively solves the function overloading problem illustrated earlier:
#include <iostream>
using namespace std;
void displayValue(int num) {
cout << "Integer version called" << endl;
}
void displayValue(char* str) {
cout << "Pointer version called" << endl;
}
void displayValue(char c) {
cout << "Character version called" << endl;
}
int main() {
displayValue(0); // Calls displayValue(int)
displayValue(NULL); // Calls displayValue(int)
displayValue(nullptr); // Calls displayValue(char*)
return 0;
}
In this updated example, the compiler treats nullptr distinctively. It cannot be converted to an integer, so the displayValue(int) overload is not selected. Instead, its implicitly converted to char*, correctly invoking the pointer version of the function. Adopting nullptr in C++11 and later standards leads to more robust, type-safe code by clearly distinguishing between null pointers and integer values.