Representing Absent Pointers with nullptr
Represent absent pointers with nullptr and check for validity before dereferencing.
What Are Null Pointers?
A null pointer is a pointer that has been set to the null value: the single value every pointer type reserves to say that it currently refers to no object. It is not a pointer to some special object, and it is not a broken pointer. It is a pointer that is deliberately empty, and C++ guarantees that your code can always ask whether a pointer is in that state.
That guarantee is the entire reason null pointers exist. A pointer variable is just storage for an address, and storage that holds no meaningful address is a hazard. A null value turns "there is no object here" into something a conditional can inspect before anything is dereferenced.
The previous lesson introduced pointers as objects that hold the address of another object, with the dereference operator * reading whatever lives at that address. Here a wheel is clamped into a repair stand and its spoke tension is read both directly and through a pointer:
#include <iostream>
int main()
{
int spokeTension{104}; // tension in the drive-side spokes, in kgf
std::cout << "on the bench: " << spokeTension << " kgf\n";
int* tensionLink{&spokeTension}; // tensionLink holds spokeTension's address
std::cout << "through the pointer: " << *tensionLink << " kgf\n";
return 0;
}
This prints:
on the bench: 104 kgf
through the pointer: 104 kgf
Dereferencing tensionLink was safe because it was handed the address of a live object at the moment it was created. This lesson is about every pointer that is not in that happy position.
The Three States a Pointer Can Be In
At any moment a pointer variable sits in exactly one of a small number of states. Laying them out as a table makes the whole lesson fall out of one column:
| State | How a pointer gets there | Can your code detect it? | Safe to dereference? |
|---|---|---|---|
| Valid | given the address of an object that is still alive | Yes, it tests as non-null | Yes |
| Null | value initialized, or assigned nullptr |
Yes, it tests as null | No |
| Dangling | the object it pointed at has since been destroyed | No | No |
| Wild | declared with no initializer at all, so it holds garbage | No | No |
Look at the third column. A conditional splits this table in exactly one place: null on one side, everything else on the other. It cannot tell a valid pointer from a dangling or wild one, because all three simply hold some address that is not the null value.
Every practice in the rest of this lesson is a way of working around that single limitation. If null is the only unsafe state you can detect, then null has to become the only unsafe state your program ever contains.
Two Ways to Spell Nothing
There are two spellings for putting a pointer into the null state, and they mean exactly the same thing.
The easiest is value initialization, an empty pair of braces. The other is the keyword nullptr, a literal whose entire job is to name the null value. It can initialize a pointer, it can be assigned to one later, and it can be handed to a function as the "nothing here" argument for a pointer parameter.
#include <iostream>
int main()
{
int* tensionLink{}; // value initialization: tensionLink is a null pointer
int* wearLink{nullptr}; // the same state, written with the literal
if (tensionLink == nullptr && wearLink == nullptr)
std::cout << "both stands start out empty\n";
int spokeTension{104};
tensionLink = &spokeTension; // tensionLink now points at a real object
std::cout << "front stand holds " << *tensionLink << " kgf\n";
return 0;
}
This prints:
both stands start out empty
front stand holds 104 kgf
Note that starting out null is not a life sentence. Assignment can move a pointer from the null state to the valid state and back again, which is what makes an empty stand a useful thing to model in the first place.
Value initialize every pointer you are not able to initialize with the address of a valid object. An empty pair of braces costs nothing and removes the wild state from your program entirely.
Spell every null pointer literal `nullptr`, in initializers, in assignments, and in argument lists alike.
`nullptr` is not tied to any one pointer type. The same literal works for an `int*`, a `double*`, or a pointer to a type you have not written yet.
What Is Legal on a Null Pointer
Null pointers are not radioactive. Plenty of operations on them are perfectly well defined, and it is worth knowing exactly where the line is:
| Operation on a null pointer | Verdict |
|---|---|
Comparing it with == or != against another pointer |
Well defined |
Testing it in a conditional, or converting it to bool |
Well defined |
| Copying it, or passing it to a function | Well defined |
| Assigning it a new address | Well defined |
Dereferencing it with * |
Undefined behavior |
Reaching a member through it with -> |
Undefined behavior |
| Adding or subtracting a non-zero offset | Undefined behavior |
Everything on the safe half of that table has one thing in common: it inspects or replaces the pointer's own value. Everything on the unsafe half tries to reach the object on the far end, and there is no object on the far end.
The following program is broken. It compiles cleanly, it will most likely crash when run, and it has no defined output, so none is shown:
#include <iostream>
int main()
{
int* tensionLink{}; // nothing is clamped in the stand
std::cout << *tensionLink << '\n'; // undefined behavior: there is no object to read
return 0;
}
The standard defines the result of unary * as the object that the operand points to. A null pointer value is defined as pointing at no object whatsoever, so there is no result for the language to produce, and the standard therefore places no requirement at all on what the program does next. That absence of any requirement is precisely what undefined behavior means. A crash is the common outcome because most operating systems refuse to map the memory page containing address zero, but a crash is not promised to you. An optimizer that has already deduced the pointer is null is entitled to delete the surrounding code, and the program may then do something far stranger than stopping.
Dereferencing a null pointer is undefined behavior, not a runtime error you can catch or recover from. Accidental dereferences of null and dangling pointers account for a large share of the crashes C++ programs produce in the field.
Guard Before You Dereference
Because a null pointer is the one unsafe state you can detect, the fix is mechanical: test before you dereference. There are two ways to write that test.
The explicit form compares the pointer against nullptr. The implicit form leans on a built-in conversion: used where a bool is wanted, a pointer yields false when it is null and true in every other case. The two functions below are interchangeable.
#include <iostream>
void reportExplicit(int* tensionLink)
{
if (tensionLink != nullptr) // compare against the literal
std::cout << "explicit check: " << *tensionLink << " kgf\n";
else
std::cout << "explicit check: stand empty\n";
}
void reportImplicit(int* tensionLink)
{
if (tensionLink) // let the pointer convert to bool
std::cout << "implicit check: " << *tensionLink << " kgf\n";
else
std::cout << "implicit check: stand empty\n";
}
int main()
{
int spokeTension{118};
int* frontLink{&spokeTension};
int* rearLink{};
reportExplicit(frontLink);
reportImplicit(frontLink);
reportExplicit(rearLink);
reportImplicit(rearLink);
return 0;
}
This prints:
explicit check: 118 kgf
implicit check: 118 kgf
explicit check: stand empty
implicit check: stand empty
Pick whichever reads better in context. The explicit comparison spells out that a pointer is being tested rather than a number; the implicit conversion is shorter and is idiomatic in modern code.
A conditional answers one question and one question only: is this pointer null? It cannot tell you whether a non-null pointer still refers to a living object. Passing the guard is not proof that dereferencing is safe.
A Dangling Pointer Walks Straight Through the Guard
Here is that warning made concrete. The following program is broken, and again no output is shown because it has none that is defined:
#include <iostream>
int main()
{
int* tensionLink{};
{
int spokeTension{92};
tensionLink = &spokeTension; // the object dies at the closing brace below
} // spokeTension is destroyed here; tensionLink is left holding its old address
if (tensionLink) // still true: the pointer is not null
std::cout << *tensionLink << '\n'; // undefined behavior: the object is gone
return 0;
}
The guard is present, correct, and useless. spokeTension is destroyed when the inner block ends, but nothing goes back and clears tensionLink. The pointer still holds the address the object used to occupy, so it converts to true, the branch is taken, and the dereference is undefined behavior.
Because this particular mistake is contained in a single function, the compiler can see through it and does complain:
s.cpp: In function 'int main()':
s.cpp:13:38: warning: dangling pointer 'tensionLink' to 'spokeTension' may be used [-Wdangling-pointer=]
13 | std::cout << *tensionLink << '\n'; // undefined behavior: the object is gone
| ^~~~
s.cpp:8:13: note: 'spokeTension' declared here
8 | int spokeTension{92};
| ^~~~~~~~~~~~
Do not read that warning as a safety net. Move the pointer into a data member, or across a function boundary, and the compiler loses sight of the object's lifetime entirely. The diagnostic is a lucky catch in an easy case, not a general defense.
Destroying an object does not null the pointers that referred to it. They are left dangling, holding a stale address, and they will keep testing as non-null. Detecting and clearing them is your job, not the compiler's and not the runtime's.
The repair is to close the gap yourself: set the pointer to nullptr at the moment its object stops being valid, so the state the guard can detect is the state the pointer is actually in.
#include <iostream>
void reportTension(int* tensionLink)
{
if (tensionLink)
std::cout << "reading " << *tensionLink << " kgf\n";
else
std::cout << "stand empty\n";
}
int main()
{
int* tensionLink{};
{
int spokeTension{92};
tensionLink = &spokeTension;
reportTension(tensionLink);
tensionLink = nullptr; // clear it while spokeTension is still alive
}
reportTension(tensionLink); // the guard now sees a null pointer
return 0;
}
This prints:
reading 92 kgf
stand empty
Nothing about reportTension changed. What changed is that the program stopped producing a state the guard was unable to see.
Hold every pointer to exactly one of two things: the address of a valid object, or `nullptr`. Maintain that rule and a single null test is enough, because any non-null pointer is one you can trust.
The Older Spellings: 0 and NULL
Code written before C++11 had no nullptr, and you will still meet the two substitutes it used. Both are shown here so you can recognize them, and neither belongs in code you write today.
The first is a plain 0. Where a pointer is expected, the compiler reads that integer literal as a request for the null value rather than as an address. The second is NULL, which is not a keyword at all but a macro that C bequeathed to C++, and it arrives with the <cstddef> header.
#include <cstddef> // for NULL
#include <iostream>
int main()
{
int* legacyZero{0}; // pre-C++11 spelling of a null pointer
int* legacyMacro{NULL}; // the C macro, inherited through <cstddef>
int* modernLink{nullptr};
if (legacyZero == modernLink && legacyMacro == modernLink)
std::cout << "all three spellings produce the same null pointer\n";
return 0;
}
This prints:
all three spellings produce the same null pointer
The same three spellings also work on the right of a comparison, which is why older code is full of if (link == NULL) and if (link == 0). Those tests are valid and do the right thing. They are simply less clear than if (link == nullptr) or the bare if (link), and 0 and NULL carry problems with function overloading that the later lesson on pass by address covers.
Most implementations really do represent a null pointer using the bit pattern of address zero, which is how the literal `0` came to work at all. The standard does not require it. Treat "null" as a state a pointer is in, not as an address you can count on.
Prefer `nullptr` over `0` and `NULL` in new code. Reach for the older forms only when you are reading or maintaining code that already uses them.
Choosing Between a Reference and a Pointer
References and pointers both let you reach an object indirectly, so a fair amount of the time either would compile. The difference is what each one lets you do wrong:
| Capability | Reference | Pointer |
|---|---|---|
| Must be bound to an object when created | Yes | No |
| Can be repointed at something else later | No | Yes |
| Can represent "nothing" | No | Yes, as a null pointer |
| Needs a null check before use | No | Yes |
| Can end up dangling | Possible, but awkward to arrange | Easy to arrange by accident |
The two rows in the middle are the pointer's whole advantage, and they are also the source of every problem in this lesson. A pointer can be null, so it can be dereferenced while null. A pointer can be reseated, so it can be aimed at an object that is about to die.
A reference has neither capability, which is why the version below has no guard and needs none:
#include <iostream>
void trueWheel(int& spokeTension) // a reference is always bound to a live object
{
spokeTension += 6;
}
int main()
{
int spokeTension{104};
trueWheel(spokeTension); // no address to pass, and no null case to guard
std::cout << "after truing: " << spokeTension << " kgf\n";
return 0;
}
This prints:
after truing: 110 kgf
There is no null reference to test for, no reseating to lose track of, and the object must exist before the reference can be formed at all.
Favor references over pointers. Use a pointer when you genuinely need one of its extra capabilities: the ability to represent "no object", or the ability to change what is being referred to.
Summary
- A null pointer holds a null value, meaning it is not pointing at anything; it is not a pointer to address zero and not a pointer to a special object
- The easiest way to make one is value initialization,
int* link{}; thenullptrliteral is the explicit spelling and works for any pointer type - A pointer is valid, null, dangling, or wild, and a conditional can only separate null from the other three
- Comparing, testing, copying, and reassigning a null pointer are all well defined; dereferencing it, reaching through it with
->, or offsetting it by a non-zero amount are undefined behavior - A pointer converts to
boolon its own:falsewhen null,truewhen non-null, soif (link)andif (link != nullptr)are equivalent guards - Destroying an object leaves pointers to it dangling; they are not nulled for you, and they will still pass a null check
- Keep every pointer either valid or
nullptr, so that one null test is enough to trust a pointer - Prefer
nullptrto the legacy0andNULL, both of which still work but say less - Favor references, which cannot be null and cannot be reseated, unless you specifically need what a pointer offers
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Representing Absent Pointers with nullptr - Quiz
Test your understanding of the lesson.
Practice Exercises
Null Pointers
Learn to work safely with null pointers. Understand nullptr, how to check for null pointers, and why you should always initialize pointers and avoid dangling pointers.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!