Null Pointers and nullptr
What a null pointer is
A null pointer is a pointer that holds no valid address, on purpose. In other words, it points at nothing. This is different from an uninitialized pointer, as you’ll see shortly.
In C++ you make a pointer null by assigning it the nullptr keyword. As it’s a keyword, you don’t need an #include to use it. You can also assign it to any pointer type: int*, double*, const std::string*, and so on.
Depending on the compiler, this prints 0 or a row of zeros instead of the address of a variable.
Here’s a null pointer declared next to an uninitialized one:
int* mystery; // Uninitializedint* empty = nullptr; // Null pointermystery holds whatever address was left in that memory. It might look perfectly valid, and there’s no way to test it. empty holds a known value that you can check for, as you’ll see next.
Checking for null
C++ makes no guarantees about what happens when you dereference a null pointer. So before you dereference a pointer, make sure it’s not null:
if (ptr != nullptr) { // Safe to use *ptr here}Pointers can also be used directly as conditions. A null pointer converts to false, and any other pointer converts to true. So this means the same as the check above:
if (ptr) { /* Not null */ }You can also check a pointer and use it in the same condition. Due to short-circuiting, *ptr only runs if ptr isn’t null:
if (ptr != nullptr && *ptr > 30.0) { std::cout << "Critical temperature\n";}Use case 1: Returning nullptr to mean “not found”
Think about this problem. You’re writing a function that goes through a list of numbers and returns the first one that’s greater than a given threshold. What should it return when no such number is found?
You might pick a special value like -1.0 to mean “not found.” A value used this way is called a sentinel. The problem is that a value like -1.0 could also be an actual number in the list, so the caller can’t tell whether it means “not found” or a real match.
Returning a pointer is one way to avoid this. The function returns a pointer to the matching number, or a null pointer if there isn’t one. This is a common pattern, especially in C-style code.
In the loop (line 7), n is declared as a reference to an element inside the vector, so &n on line 9 is that element’s address. Returning it is safe, because the element belongs to the caller’s vector and still exists after the function returns.
Returning a pointer gives you two things at once. The null case clearly says “nothing found,” and in the found case you get the actual element.
Use case 2: Passing nullptr to a function
A function can only return one value. If it needs to return a second value, it can take a pointer parameter instead and then store the value at the address held by that pointer.
A caller who doesn’t need the second value can pass nullptr instead.
Here’s a function that averages everyone’s answers to one survey question, rated from 1 to 5. If a person skips the question, their rating is recorded as -1. Besides returning the average, the function can also report how many people skipped the question:
The function checks skippedCount before storing anything through it, because the caller is allowed to pass nullptr.
To let callers leave out the argument entirely, give the parameter a default: int* skippedCount = nullptr.
Why not NULL or 0?
Before C++11, nullptr didn’t exist. In older code, you’ll see null pointers written like this instead:
int* a = 0;int* b = NULL;The plain integer 0 doubled as the null pointer. NULL is a macro, a name that gets replaced with some predefined text by the preprocessor (before the code is compiled). For NULL, that replacement text is the integer 0 or 0L (a zero of long type).
Because 0 and NULL are both integers, a problem arises when a function is overloaded so that one overload accepts an integer and the other accepts a pointer. Passing 0 to mean “null pointer” calls the integer version instead (line 19):
If NULL is passed (by uncommenting line 20), some compilers pick the int version and others give a compile-time error because they can’t decide which overload to pick.
nullptr fixes this by having its own type, std::nullptr_t. That type converts to any pointer type but never to an integer. So logEvent(nullptr) can only match the pointer version (line 21).
Checkpoint
What goes wrong when this program runs?
#include <vector>
void minMax(const std::vector<int>& v, int* minOut, int* maxOut = nullptr) {
int smallest = v[0];
int largest = v[0];
for (int x : v) {
if (x < smallest) smallest = x;
if (x > largest) largest = x;
}
*minOut = smallest;
*maxOut = largest;
}
int main() {
std::vector<int> scores = {72, 88, 65, 91};
int lowest = 0;
minMax(scores, &lowest);
}What’s the problem with this code?
int* p;
if (p != nullptr) {
std::cout << *p;
}