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?
- 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.
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?
- x
- 42
- y
- 7
Two independent integers
x and y are separate objects. Changing one does not change the other.
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?
- Elements
- 5
- Illustrated sizeof(int)
- 4 bytes
Contiguous array storage
The five integers occupy adjacent elements. This illustration assumes sizeof(int) is 4 bytes.
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?
- ptr
- nullptr
- alias
- nullptr
Null is an explicit empty state
Neither pointer designates an object. Never dereference a null pointer.
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.
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:
- Use a reference for a required object. Use
T&when the function may modify the caller's object, orconst T&for read-only access to an object that would be expensive to copy. Small scalar values are often better passed by value. - Use a pointer for an optional object. A documented
nullptrstate makes absence explicit. Check for null before accessing an optional target. - Keep ownership explicit. Prefer scope-bound objects and containers. Use
std::unique_ptrfor exclusive dynamic ownership, andstd::shared_ptrwhen ownership truly needs to be shared. - 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:
// 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
- C++ working draft: references
- C++ working draft: pointer arithmetic
- C++ Core Guidelines: resource management
Related concepts
Follow a C++ function from tokens to a simplified AST, then inspect exact source ranges, declaration references and types in a keyboard-accessible explorer.
Understand the complete C++ compilation pipeline from source code to object files. Learn preprocessing, parsing, code generation, and optimization stages.
Master C++ OOP concepts including inheritance, polymorphism, virtual functions, and modern object-oriented design principles with interactive examples.
C++ preprocessor visualized: macros, header guards, conditional compilation, and #include directives explained interactively.
Learn Resource Acquisition Is Initialization (RAII) - the cornerstone of C++ memory management. Understand automatic resource cleanup and exception safety.
Master C++11 smart pointers through interactive examples. Learn unique_ptr, shared_ptr, and weak_ptr with reference counting visualizations.
