Dataslope logoDataslope

Pointers and References

The two ways to name memory in C++ — when to use which, common bugs, and how to picture them.

A pointer holds a memory address. A reference is an alias for an existing variable. Both are how C++ lets one piece of code reach into memory owned by another. They are the two tools you will use most often once functions and objects start sharing data.

Beginners often find pointers intimidating. Don't be. A pointer is just a number — the address of a byte in memory — with a type attached so the compiler knows how to interpret what's there.

A pointer is an address with a type

int  n = 42;
int* p = &n;     // p holds the address of n

After the second line, two variables exist:

  • n is an int somewhere on the stack, storing 42.
  • p is an int* (read: "pointer to int"), storing the address of n.

You read what a pointer points to with the dereference operator *: *p means "the int at the address held by p."

Code Block
C++ 20 (202002L)

Two operators do most of the pointer work:

  • &x — take the address of x. Type: T* if x is T.
  • *p — dereference p to read or write the pointed-to value.

nullptr: the pointer that points to nothing

A pointer that holds the special value nullptr does not point at any object. Dereferencing it is undefined behavior — usually a crash. Initialize every pointer to something (often nullptr) and check before dereferencing.

Code Block
C++ 20 (202002L)

Older code uses 0 or NULL instead of nullptr. Always use nullptr in modern C++: it has its own type and avoids ambiguities.

References: aliases, not pointers

A reference is a second name for an existing variable. Once bound, it cannot be re-bound to anything else.

int n = 42;
int& r = n;   // r is now another name for n
r = 100;      // modifies n
std::cout << n;  // prints 100

Key differences from pointers:

PointerReference
Can be nullptrMust refer to a real object
Can be reassigned to point elsewhereBound once, for life
Has its own storage (the address)Conceptually free; usually compiled as a hidden pointer
Syntax: *p, p->mSyntax: just r, r.m

You use references most often as function parameters (which we covered in the functions chapter) and as return types when you want the caller to be able to modify a member of an object.

Code Block
C++ 20 (202002L)

When to use which

A rough but reliable rule:

  • By value if the type is cheap to copy and you don't need to modify the caller.
  • Const reference if the type is expensive to copy and you don't need to modify it.
  • Non-const reference if you must modify the caller's object.
  • Pointer if the parameter is optional (so it can be nullptr) or if you want to express "I might point at a different object later."
  • Smart pointer (std::unique_ptr, std::shared_ptr) when you actually own a heap object. We'll cover these soon.

Pointer arithmetic and arrays

Arrays in C++ live in contiguous memory. You can do arithmetic on pointers to step through them.

Code Block
C++ 20 (202002L)

Pointer arithmetic moves in steps of the pointed-to type's size: p + 1 advances by sizeof(int) bytes, not by 1 byte. This is exactly what makes array indexing work.

In modern C++ we usually prefer std::vector over raw arrays — it carries its own size, grows itself, and frees itself. Pointer arithmetic is then internal to the implementation, not your daily code.

-> is just (*p). shorthand

When p is a pointer to a struct or class, you access its members with ->:

struct Point { int x; int y; };
Point pt{3, 4};
Point* p = &pt;
std::cout << (*p).x << "\\n";   // works but verbose
std::cout << p->x   << "\\n";   // same thing, idiomatic

Const + pointers: the four shapes

Mixing const with pointers trips up everybody at first. Read right-to-left:

int* p;                  // pointer to int. Can change both.
const int* p;            // pointer to const int. Can't write *p; CAN reassign p.
int* const p;            // const pointer to int. Can write *p; CAN'T reassign p.
const int* const p;      // const pointer to const int. Can change nothing.

The compiler enforces this for you, which is why const everywhere is so valuable: it documents intent and catches mistakes.

Common pointer/reference bugs

Almost every C++ runtime crash is one of these:

BugCauseFix
Null derefReading *p when p == nullptrCheck before dereferencing.
Dangling pointerThe pointed-to object died but the pointer still points thereDon't return addresses of locals; reset pointers after delete.
Use-after-freeDereferencing memory that has been deletedUse smart pointers (later chapter).
Double-freeCalling delete twice on the same addressUse smart pointers; set raw pointers to nullptr after delete.
Wild pointerReading an uninitialized pointerInitialize to nullptr.

A worked example: swap, two ways

We saw swap_ints by reference. Here it is again, both with references and with pointers, so you can compare.

Code Block
C++ 20 (202002L)

The reference version is simpler at the call site (swap_ref(x, y) versus swap_ptr(&x, &y)) and safer (it can't be null). Reach for references unless you really need to express "this might be absent."

Challenges

Challenge
C++ 20 (202002L)
Add one through a pointer

Implement void add_one(int* p). If p is not null, increment the int it points to by 1. If p is null, do nothing. The provided main calls it on a real address and prints the resulting value; you should see exactly 8.

Challenge
C++ 20 (202002L)
Sum a C-array with pointer arithmetic

Implement int sum(const int* data, int n) that returns the sum of the first n ints starting at data. Use pointer arithmetic (or indexing — they're equivalent). main runs the function on {1, 2, 3, 4, 5} and should print 15.

Test your understanding

QuestionSelect one

What is a pointer, in the simplest physical sense?

A label for a function.

A copy of an object stored elsewhere.

A variable whose value is the memory address of another object, paired with a type that describes what's at that address.

A type-erased reference to any value.

QuestionSelect one

Which of the following is not true of references?

They must be initialized when declared.

You can later make a reference refer to a different object by assigning to it.

They cannot be null.

They are usually compiled to a hidden pointer.

QuestionSelect one

Why is dereferencing a nullptr undefined behavior?

The C++ standard requires it to throw an exception.

The OS will always send SIGSEGV.

Because nullptr does not point to any valid object, the language places no restrictions on what happens; the compiler is allowed to do anything.

It is well-defined; it always returns 0.

QuestionSelect one

What does p->x mean, given a pointer p to a struct with member x?

It is the same as p.x.

It allocates a new field x on p.

It is shorthand for (*p).x: dereference the pointer, then access member x.

It is undefined behavior.

Next: how to ask the operating system for memory you really need — dynamic memory with new, delete, and the modern-C++ tools that automate them.

On this page