Skip to content

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.

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

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

Best Practice

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++.

Pitfall

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.

ptr_vs_ref.cpp
#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
}
Key Takeaways
  • & 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 const placement right-to-left: const int* vs int* 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++?