Pointers: Addresses, Dereferencing & Arithmetic
Understand pointers as memory addresses, learn address-of and dereference operators, pointer arithmetic within arrays, const correctness with pointers, and when raw pointers are still appropriate in modern C++.
What Is a Pointer?
A pointer is a variable that stores a memory address. Every object in C++ lives at some address in memory, and a pointer lets you refer to that object indirectly. Pointers are fundamental to C++ — they enable dynamic data structures, polymorphism, and efficient parameter passing.
A pointer type is declared with *: int* p; declares a pointer to an int. The pointer itself occupies memory (typically 8 bytes on 64-bit systems) and its value is an address, not the data it points to.
Address-Of & Dereference
The two fundamental pointer operators are & (address-of) and * (dereference). The & operator obtains the address of a variable. The * operator follows the address to access the pointed-to object.
#include <iostream>
int main() {
int value = 42;
int* ptr = &value; // ptr holds the address of value
std::cout << "value: " << value << '\n'; // 42
std::cout << "&value: " << &value << '\n'; // e.g. 0x7ffd5a3c
std::cout << "ptr: " << ptr << '\n'; // same address
std::cout << "*ptr: " << *ptr << '\n'; // 42 (dereference)
*ptr = 100; // modify value through the pointer
std::cout << "value after *ptr = 100: " << value << '\n'; // 100
int* null_ptr = nullptr; // points to nothing — safe sentinel
// *null_ptr = 5; // UNDEFINED BEHAVIOR — crash on most systems
}Pointer Arithmetic & Array Decay
Pointer arithmetic is only valid within an array (or one past the end). The compiler scales the arithmetic by sizeof(T). An array name decays to a pointer to its first element in most contexts — this is why C-style arrays lose their size information when passed to functions.
#include <iostream>
#include <cstddef> // std::ptrdiff_t
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int* p = arr; // array decays to pointer to first element
std::cout << *p << '\n'; // 10
std::cout << *(p + 2) << '\n'; // 30 (moves 2*sizeof(int) bytes)
std::cout << p[3] << '\n'; // 40 (p[i] is *(p + i))
// Iterating via pointer
for (int* it = arr; it != arr + 5; ++it) {
std::cout << *it << ' ';
}
std::cout << '\n'; // 10 20 30 40 50
// Pointer difference
std::ptrdiff_t diff = (arr + 4) - arr; // 4 (elements, not bytes)
std::cout << "diff: " << diff << '\n';
// WARNING: arithmetic outside the array is UNDEFINED BEHAVIOR
// int* bad = arr + 10; // UB — don't do this
}const Pointers vs Pointer-to-const
The placement of const relative to * determines what is immutable:
- const int* p (or int const* p) — pointer to const: you cannot modify the pointed-to value through p, but you can re-seat p to point elsewhere.
- int* const p — const pointer: the pointer itself is immutable (cannot re-seat), but you can modify the value it points to.
- const int* const p — both the pointer and the pointed-to value are immutable.
Read declarations right to left: int* const p reads as "p is a const pointer to int."
Void pointers (void*) can hold any address but cannot be dereferenced without casting. They are primarily used in C-style APIs and allocator implementations.
In modern C++, raw pointers should be used exclusively as non-owning, observer pointers. They say: "I can see this object, but I am not responsible for its lifetime." Common legitimate uses:
- Implementing data structures (tree nodes pointing to parent)
- Function parameters that observe but don't own (though references are often better)
- Interfacing with C APIs
- Iterators in custom containers
If a pointer owns a resource, wrap it in std::unique_ptr or std::shared_ptr. If you find yourself writing delete, you are almost certainly doing it wrong in modern C++.
Dangling pointers are the #1 pointer bug. A pointer becomes dangling when the object it points to is destroyed:
- Returning a pointer to a local variable
- Deleting an object while another pointer still references it
- Pointer into a std::vector that is reallocated after push_back
Uninitialized pointers contain garbage addresses. Always initialize to nullptr if you don't have a target yet. The compiler will NOT warn about all uninitialized pointer uses.
Pointer vs Reference: When to Use Which
References and pointers both provide indirection, but they differ in important ways. References cannot be null, cannot be re-seated, and don't require explicit dereferencing. Prefer references for function parameters; use pointers when nullability or re-seating is needed.
#include <iostream>
void increment_ref(int& val) { ++val; } // cannot be null
void increment_ptr(int* val) { if (val) ++(*val); } // must null-check
int main() {
int x = 10;
increment_ref(x); // clean call syntax
increment_ptr(&x); // caller must take address
std::cout << x << '\n'; // 12
int* p = &x;
p = nullptr; // OK — pointers can be re-seated and nulled
int& r = x;
// r = ???; // references cannot be re-seated
// int& bad; // ERROR — references must be initialized
}&gets an address,*follows it — these are inverse operations- Pointer arithmetic is only valid within arrays and scales by
sizeof(T) - Array names decay to pointers, losing size information
- Read
constplacement right-to-left:const int*vsint* const - In modern C++, raw pointers should be non-owning observers only
- Prefer references over pointers unless you need nullability or re-seating
Quiz — Test Your Knowledge
(15 XP)1. What does `const int* p` mean?
2. What happens when an array is passed to a function expecting a pointer parameter?
3. When is pointer arithmetic valid in C++?