TRT
Sign in

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.

FreeAbout 8 minutes

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

What assignment does to a reference and to a pointerstep 1 of 6
int a = 1;int b = 2;int& r = a;r = b;int* p = &a;p = &b;p = nullptr;
  1. var 0a @1000: 1
  2. var 1b @1004: 2
  3. var 2r: --
  4. 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:

cpp
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:

cpp
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:

cpp
void show(int n);
void show(const char* s);
 
show(0);         // calls show(int)
show(nullptr);   // calls show(const char*), as intended

With 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

Something wrong with this lesson? Report a problem

Not started

Sign in to save your learning progress.

Sign in to save