Pointers
Variables that store memory addresses, enabling indirection, dynamic allocation, and polymorphism across all C++ versions.
Pointersince C++98A pointer is a scalar object that stores the address of another object or function in memory, providing a level of indirection that enables dynamic allocation, polymorphism, and efficient data passing without copying.
Overview
Every pointer value is in exactly one of four states:
- Points to an object or function β valid for read/write (respecting cv-qualifiers)
- Points past the end of an object β valid for comparison and arithmetic, illegal to dereference
- Null pointer value β indicates "no object"; compares equal to all null pointers of the same type
- Invalid (indeterminate) β uninitialized or dangling; any use is undefined behavior
Dereferencing anything but "points to an object or function" is undefined behavior. This is the root cause of the majority of hard-to-reproduce memory-safety bugs in C++ programs.
Null pointer representation before and after C++11
Prior to C++11, null pointers were expressed with the integer literal 0 or the macro NULL (from <cstddef>). Both carry ambiguity: NULL expands to 0 or (void*)0 depending on the implementation, and either form can silently match an integer overload instead of a pointer overload during overload resolution.
C++11 introduced nullptr, a keyword of type std::nullptr_t. It converts implicitly to any pointer type but never to an integer. Use nullptr exclusively in all code targeting C++11 and later.
Syntax
T* ptr; // pointer to T β uninitialized, indeterminate value
T* ptr = nullptr; // null pointer (C++11)
T* ptr = &obj; // pointer to obj
*ptr // dereference β yields an lvalue of type T
ptr->member // member access through pointer (equivalent to (*ptr).member)
ptr + n // advances ptr by n objects (n * sizeof(T) bytes)
ptr2 - ptr1 // signed distance in elements (ptrdiff_t), only within same arrayThe asterisk binds to the declarator, not the base type:
int* p, q; // p is int*, q is plain int β common declaration trap
int *p, *q; // both int* β explicit form avoids ambiguityconst placement and pointer types
Where const appears relative to * determines what cannot be modified. Reading the declaration right-to-left gives the correct reading:
const int* p; // pointer to const int β *p is read-only, p itself can change
int* const p; // const pointer to int β p cannot be reseated, *p can change
const int* const p; // both pointer and pointee are constExamples
Pointer arithmetic over a range
#include <cstddef>
#include <cstdio>
int sum(const int* data, std::size_t n) {
int total = 0;
for (const int* end = data + n; data != end; ++data)
total += *data;
return total;
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
printf("%d\n", sum(arr, 5)); // 15
}Array names decay to a pointer to their first element in most expression contexts (array-to-pointer conversion). The pointer data + n is a valid past-the-end pointer β legal to compute and compare against, illegal to dereference.
Runtime polymorphism through base-class pointers
struct Shape {
virtual double area() const = 0;
virtual ~Shape() = default;
};
struct Circle : Shape {
double r;
explicit Circle(double radius) : r(radius) {}
double area() const override { return 3.14159265 * r * r; }
};
struct Rect : Shape {
double w, h;
Rect(double w, double h) : w(w), h(h) {}
double area() const override { return w * h; }
};
void print_area(const Shape* s) {
printf("area = %.4f\n", s->area());
}Virtual dispatch requires that the pointer points to a complete, live object. Calling a virtual function through a null or dangling pointer is undefined behavior β the vtable lookup itself is just an indirect load, but the compiler is entitled to assume valid state.
void* and placement new
void* is the generic pointer type. Any non-function, non-member pointer converts implicitly to void*. It cannot be dereferenced or used in arithmetic without a cast. It underpins malloc/free and placement new:
#include <cstdlib>
#include <new>
struct Expensive { int data[64]; };
void* raw = std::malloc(sizeof(Expensive));
Expensive* obj = ::new (raw) Expensive{}; // placement new (C++98)
obj->~Expensive(); // explicit destructor required
std::free(raw);Overload resolution with nullptr vs 0 (C++11)
void f(int n) { printf("int %d\n", n); }
void f(char* p) { printf("char* %p\n", static_cast<void*>(p)); }
f(0); // calls f(int) β 0 is an integer
f(NULL); // calls f(int) β NULL is typically 0, implementation-defined
f(nullptr); // calls f(char*) β C++11: nullptr_t converts to char*, not intstd::to_address for generic pointer-like types (C++20)
In templates, avoid assuming every pointer-like type can be directly compared with %p. std::to_address (C++20) extracts the raw pointer from any contiguous iterator or fancy pointer without dereferencing:
#include <memory> // std::to_address β C++20
template<typename Ptr>
void log_address(Ptr p) {
printf("raw address: %p\n",
static_cast<const void*>(std::to_address(p))); // C++20
}Best Practices
Reserve raw pointers for non-owning handles. A raw pointer should communicate "I observe this object but don't own it." For owned heap resources, use std::unique_ptr (C++11) or std::shared_ptr (C++11). This eliminates entire classes of leaks and double-frees without runtime cost for unique_ptr.
Initialize every pointer at the point of declaration. An uninitialized pointer contains a garbage address that silently passes null checks. Declare with = nullptr or = &obj; never leave the initial value implicit.
Prefer references when nullptr is not a valid state. References cannot be null and cannot be reseated. Using a reference in a function signature communicates "this parameter is always a valid object," which is a stronger contract and eliminates null checks inside the callee.
Keep pointer arithmetic strictly within array bounds. Arithmetic is only defined within the same array object (including the one-past-the-end position) and for a single object treated as a 1-element array. Going outside these bounds is UB regardless of memory mapping.
Common Pitfalls
Dangling pointers
A pointer becomes dangling when its target is destroyed β stack unwind, explicit delete, or end-of-scope. Accessing a dangling pointer is UB:
int* get_local() {
int x = 42;
return &x; // x is destroyed on return; caller receives a dangling pointer
}More subtle variants arise when a raw pointer into a std::vector survives a reallocation:
std::vector<int> v = {1, 2, 3};
int* p = v.data();
v.push_back(4); // may reallocate β p is now dangling
printf("%d\n", *p); // UBReserve raw pointers into containers for spans of code where no insertion or erasure can occur.
Type punning through reinterpret casts
Casting between unrelated pointer types and dereferencing violates the strict aliasing rule. The standard permits aliasing only through char*, unsigned char*, and (since C++17) std::byte*. For type-safe bit-reinterpretation use std::memcpy or, preferably, std::bit_cast (C++20):
#include <bit> // C++20
float f = 1.0f;
uint32_t bits = std::bit_cast<uint32_t>(f); // C++20 β defined, no UBSigned overflow in pointer arithmetic
Adding a signed integer that causes the pointer to step outside its array's extent is UB even if the resulting address is readable. Optimizers exploit this assumption aggressively: out-of-bounds arithmetic can cause the compiler to eliminate adjacent safety checks.
See Also
- Function Pointers β storing, comparing, and invoking functions through a pointer
- Abstract Classes β runtime polymorphism via base-class pointer dispatch
autoType Deduction βauto*and pointer type deduction in templates and range-for