Skip to content
C++
Language
Basic

Pointers

Variables that store memory addresses, enabling indirection, dynamic allocation, and polymorphism across all C++ versions.

Pointersince C++98

A 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

cpp
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 array

The asterisk binds to the declarator, not the base type:

cpp
int* p, q;    // p is int*, q is plain int β€” common declaration trap
int *p, *q;   // both int* β€” explicit form avoids ambiguity

const placement and pointer types

Where const appears relative to * determines what cannot be modified. Reading the declaration right-to-left gives the correct reading:

cpp
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 const

Examples

Pointer arithmetic over a range

cpp
#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

cpp
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:

cpp
#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)

cpp
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 int

std::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:

cpp
#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:

cpp
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:

cpp
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);  // UB

Reserve 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):

cpp
#include <bit>  // C++20

float f = 1.0f;
uint32_t bits = std::bit_cast<uint32_t>(f);  // C++20 β€” defined, no UB

Signed 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