Foundations · Module 3 · Lesson 8 of 10
Pointers Versus References, and nullptr
Compare references and pointers by what assignment does to them and by which of them can be null or re-aimed, and see how nullptr and null checks let a pointer say that there is no object.
By the end you can
- Contrast what assignment does to a reference and to a pointer
- Choose a value, a reference or a pointer for a parameter or a return type
- Explain why nullptr replaces 0 and NULL
- Guard a nullable pointer and predict what dereferencing a null pointer does
Builds on Pointers: Variables That Hold Addresses, Passing by Value, by Reference and by Const Reference.
01 · A question first
A reference can never be empty and can never be re-aimed. A pointer can be both. When a function needs to say that it found nothing, which of the two can say so? And why did C++11 invent a brand-new keyword, nullptr, to replace the humble 0?
Make a guess, then step through the trace below to check it.
02 · Watch it run
int a = 1;int b = 2;int& r = a;r = b;int* p = &a;p = &b;p = nullptr;- var 0a @1000: 1
- var 1b @1004: 2
- var 2r: --
- var 3p: --
a holds 1 and b holds 2. The four cells are a, b, a reference r that we are about to create, and a pointer p. At this moment r and p do not exist yet.
Machine work: store 1 and 2
03 · The idea
Start with the problem
C++ has two ways to refer to an object that lives elsewhere: a reference, from lesson 7 of module 2, and a pointer, from lesson 6 of this module. Underneath, both hold an address. So when do you use which?
A concrete problem shows the difference. A function searches a vector for a negative number. It should hand back the element so that the caller can change it, but sometimes there is no such element. A reference cannot say "nothing", because it must always refer to an object. A pointer can:
int* findNegative(std::vector<int>& v) {
for (int& x : v) {
if (x < 0) {
return &x;
}
}
return nullptr;
}The caller then has to check the result before using it:
int* p = findNegative(values);
if (p != nullptr) {
*p = 0;
}How the idea works
What differs. The trace compares them line by line:
- A reference is bound to an object when it is created and refers to it for its whole life (steps 2 and 3). A pointer is an ordinary variable that can be pointed somewhere else later (step 5).
- Assigning to a reference assigns to the object it names. Assigning to a pointer changes the address it holds, and assigning through the pointer with
*changes the object. - A reference cannot be null. A pointer can (step 6).
- A reference is used exactly like the object. A pointer needs
*to reach the object, or->for members, which you will meet with structs. - Pointers support arithmetic, as in lesson 7, and can be stored in arrays and containers. References cannot be elements of an array.
nullptr. The keyword nullptr, from C++11, is the null pointer constant. Its type is std::nullptr_t, and it converts to any pointer type but to no integer type. Before it, programmers wrote 0 or the macro NULL, which are integers, and that made overload resolution unreliable:
void show(int n);
void show(const char* s);
show(0); // calls show(int)
show(nullptr); // calls show(const char*), as intendedWith NULL the same call may pick the integer overload or be ambiguous, depending on the compiler. Modern code uses nullptr and nothing else.
Checking. Write if (p != nullptr), or the shorter if (p), since a null pointer converts to false. Following a null pointer is undefined behavior, and the usual outcome is a segmentation fault.
Who owns the object. A raw pointer or a reference says "I may use this object". It does not say "I must free it". Owners are containers and, in the Core level, smart pointers. Lesson 9 shows what manual ownership looks like.
Choosing. For parameters, pass cheap types by value, pass large read-only objects by const T&, and use T& when the function must modify an object that has to exist. Use T* when null is a meaningful answer, "no object here". For return values, a pointer fits "found, or not found". For a value that may be absent, std::optional<T> from C++17 holds either a value or nothing, without a pointer.
Why it is correct
The two tools exist because they give different guarantees. A reference promises that it always refers to an object, and the language enforces that by binding it at creation, forbidding rebinding and forbidding null. Code that receives a reference needs no checks. A pointer promises less and allows more: it may be null and it may change, so every use is an obligation to check. The type records the contract. A function that takes T& says that the object must exist, and one that takes T* says that it may be absent.
Since both are an address underneath, as lesson 7 of module 2 explained, the choice is about meaning, not speed. And nullptr fixes an old mistake: 0 was an integer that also worked as a pointer, which confused overload resolution and templates. A constant with its own type removes the ambiguity.
What it costs
Run time. References and pointers cost the same in practice: 8 bytes to pass, and often nothing at all after inlining, when the compiler removes the indirection. A reference member inside an object takes 8 bytes like a pointer.
Null checks. A check is one comparison and a well-predicted branch, effectively free. Forgetting it costs a crash, or worse.
Pointer chasing. Containers of pointers scatter objects in memory, with the cache cost lesson 6 described.
Aliasing. Both kinds of parameter stop the compiler from assuming that two names refer to different objects, which limits some optimizations.
Pitfalls and variations
Rebinding that is not. r = b does not make r refer to b. It copies the value, as step 3 shows. To aim at something else, you need a pointer.
Unchecked dereference. Check every pointer that may be null, and check it every time. A check on one path and not on another is a bug waiting for the other path.
Returning a reference or pointer to a local. The object dies with the function's frame, as in lesson 6 of module 2. Return a value, or a pointer to something that outlives the call.
Old null constants. Do not use NULL or 0 for pointers.
Comparing the wrong thing. p == q compares addresses. Compare the objects with *p == *q, after checking both for null.
Owning raw pointers. A raw pointer that holds the only way to free an object invites leaks, which is the subject of lesson 9.
Variations. std::reference_wrapper lets a reference be stored in a container. std::optional represents an optional value, and the smart pointers std::unique_ptr, std::shared_ptr and std::weak_ptr represent owned or shared objects, all in the Core level.
Where it is used
Linked structures use null to mark the end: the last node of a list points to nothing. C interfaces report failure by returning null, as fopen and malloc do. C++ interfaces use references for parameters that must exist, which is by far the most common form, and pointers wherever "no object" is a real possibility.
The next lesson, "The Heap: new, delete and Memory Leaks", shows where an object can live after the function that created it has returned, and what you owe the system when you ask for that.
04 · Prove it
Question 1 of 4 · answer from memory
After `int a = 1, b = 2; int& r = a; r = b;`, what are `a` and `b`, and what does `r` refer to?
05 · Practice
Asked in interviews
Something wrong with this lesson? Report a problem