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 nAfter the second line, two variables exist:
nis anintsomewhere on the stack, storing 42.pis anint*(read: "pointer to int"), storing the address ofn.
You read what a pointer points to with the dereference operator
*: *p means "the int at the address held by p."
Two operators do most of the pointer work:
&x— take the address ofx. Type:T*ifxisT.*p— dereferencepto 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.
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 100Key differences from pointers:
| Pointer | Reference |
|---|---|
Can be nullptr | Must refer to a real object |
| Can be reassigned to point elsewhere | Bound once, for life |
| Has its own storage (the address) | Conceptually free; usually compiled as a hidden pointer |
Syntax: *p, p->m | Syntax: 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.
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.
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, idiomaticConst + 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:
| Bug | Cause | Fix |
|---|---|---|
| Null deref | Reading *p when p == nullptr | Check before dereferencing. |
| Dangling pointer | The pointed-to object died but the pointer still points there | Don't return addresses of locals; reset pointers after delete. |
| Use-after-free | Dereferencing memory that has been deleted | Use smart pointers (later chapter). |
| Double-free | Calling delete twice on the same address | Use smart pointers; set raw pointers to nullptr after delete. |
| Wild pointer | Reading an uninitialized pointer | Initialize 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.
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
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.
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
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.
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.
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.
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.