Skip to main content

Pointers & References in C++

Understand C++ pointers and references with step-by-step memory diagrams: addressing, dereferencing, aliasing, array arithmetic, and object lifetime.

Understanding Pointers and References

Suppose x holds 42. How can another part of your program change that exact integer without making a copy? A pointer stores its address; a reference gives the same object another name. Both provide access to the original object, but they behave differently when assigned to.

In Python memory management, names refer to objects whose lifetimes CPython manages through reference counts and garbage collection. A C++ raw pointer does not provide that lifetime management. Understanding the address is only half the job; the object at that address must still be alive.

Key Concepts Covered

Follow x, ptr, and ref through four independently controlled examples. Each step connects an active source line to the values and memory relationships it changes. The diagrams use authored, illustrative addresses and a 4-byte int; they do not execute C++ in the browser.

Example 1

Follow an address to its value

What changes when you write through a pointer?

Step 1 / 7
ptr—not declaredx—not declarednot yet declared
x
not declared
ptr
not declared

Start with two different things

x will store an integer. ptr will be a separate object that stores the address of x.

dereference.cpp
int x = 42;int* ptr = &x;std::cout << *ptr << "\n";*ptr = 100;std::cout << x << "\n";

A reference offers another way to reach the same object. Compare assigning through ref with changing the address stored in ptr.

Example 2

One object, another name

Why can a pointer change targets while a reference stays bound?

Step 1 / 7
x420x1000y70x1004ptr—not declared
x
42
y
7

Two independent integers

x and y are separate objects. Changing one does not change the other.

reference.cpp
int x = 42;int y = 7;int& ref = x;int* ptr = &x;ref = 100;ptr = &y;ref = y;*ptr = 9;std::cout << x << " " << ref << " " << y << "\n";

Pointers can also traverse contiguous arrays. Follow the pointer as it advances, then inspect the one-past-the-end boundary.

Example 3

Move by elements, not bytes

Where does an int pointer go when you increment it?

Step 1 / 6
10arr[0]0x400020arr[1]0x400430arr[2]0x400840arr[3]0x400C50arr[4]0x4010endIllustrated stride: sizeof(int) = 4 bytes
Elements
5
Illustrated sizeof(int)
4 bytes

Contiguous array storage

The five integers occupy adjacent elements. This illustration assumes sizeof(int) is 4 bytes.

pointer_arithmetic.cpp
int arr[] = {10, 20, 30, 40, 50};int* ptr = arr;++ptr;++ptr;std::cout << *ptr << "\n";ptr = arr + 5;  // One past the end; do not dereference.

An address only helps while its object is alive. Delete the heap object below, then see why resetting one pointer leaves the other alias dangling.

Example 4

An address does not keep an object alive

What happens to aliases when a heap object is deleted?

Step 1 / 6
ptrnullptraliasnullptrheap objectno object
ptr
nullptr
alias
nullptr

Null is an explicit empty state

Neither pointer designates an object. Never dereference a null pointer.

pointer_lifetime.cpp
int* ptr = nullptr;int* alias = nullptr;ptr = new int(42);alias = ptr;delete ptr;ptr = nullptr;alias = nullptr;

Deep Dive: Pointer vs Reference

Pointers

int* ptr = &x creates a pointer object and stores the address produced by &x. The expression *ptr designates the pointed-to integer: you can read its value or assign a new one. Changing *ptr changes x; changing ptr changes which object you reach.

The pointer has storage of its own. &ptr is the address of that pointer object, while ptr is the address it holds. A pointer-to-pointer (int**) adds another level of indirection.

References

int& ref = x binds an lvalue reference to x. Reading ref reads x, assigning to ref writes x, and &ref equals &x. The language does not require a reference to occupy a separate storage slot, so the diagram gives the integer two names.

Once bound, the reference cannot be reseated. ref = y assigns the value of y to x; it does not change the binding. A reference has no valid null state, but it can dangle if its object dies. Syntax does not replace lifetime management.

This page focuses on lvalue references (T&). Rvalue references (T&&) support move semantics and forwarding. References to references cannot be declared directly; template substitution and type aliases can produce combinations handled by reference collapsing.

PointerT*
ReferenceT&
Meaning
A separate object holding an address.
Another name for an existing object.
Assignment
ptr = &y changes the target.
ref = y writes to the bound object.
Empty state
nullptr represents no object.
No valid null state; a local reference needs an initializer.
Access
*ptr accesses the pointed-to object.
ref accesses the object directly.
Lifetime
May dangle; a raw pointer does not manage lifetime.
May dangle; an alias does not generally extend lifetime.

Pointer arithmetic stays within an array

For a T*, adding one advances by one T element, corresponding to sizeof(T) bytes. The explorer uses a 4-byte int as an illustration; the size of int depends on the implementation.

For the five-element array, arr + 5 is a valid one-past-the-end pointer, useful as a loop boundary. It must not be dereferenced. Arithmetic beyond that boundary, or subtraction of pointers into unrelated arrays, is undefined behavior.

Best Practices

Choose the parameter type to express what the caller is allowed to do:

  1. Use a reference for a required object. Use T& when the function may modify the caller's object, or const T& for read-only access to an object that would be expensive to copy. Small scalar values are often better passed by value.
  2. Use a pointer for an optional object. A documented nullptr state makes absence explicit. Check for null before accessing an optional target.
  3. Keep ownership explicit. Prefer scope-bound objects and containers. Use std::unique_ptr for exclusive dynamic ownership, and std::shared_ptr when ownership truly needs to be shared.
  4. Keep array bounds available. Prefer range-based loops, containers, or std::span (C++20) when passing a contiguous range. A raw pointer alone does not carry its length.

Non-null does not mean valid

A dangling pointer can still contain a nonzero address. if (ptr) only checks for null; it cannot prove that the object is alive or that the address is within bounds. References require the same care about object lifetime.

Raw Pointer Management

The lifetime example shows the essential rule: copying a raw pointer copies an address, without creating an owner or extending the object's lifetime. If your code takes ownership with new, it must eventually release that allocation:

C++
// Manual memory management with raw pointers
int* ptr = new int(42);  // Allocate on heap
*ptr = 100;              // Modify value
delete ptr;              // Must manually free memory
ptr = nullptr;           // Clear this pointer; other aliases still dangle

// Array allocation
int* arr = new int[5];   // Allocate array
delete[] arr;            // Must use delete[] for arrays

Pair raw pointer allocation with cleanup

Pair new with delete, and new[] with delete[]. Deleting twice or accessing a destroyed object is undefined behavior; losing the last owning address without cleanup leaks the allocation. Prefer a container or smart pointer to make cleanup follow scope, including when an exception is thrown.

Further Reading

If you found this explanation helpful, consider sharing it with others.

Mastodon