Pointers

C++ · Practical · 15 min

Pointers underlie arrays, polymorphism, data structures and every C API, and you must read them fluently even if you rarely own memory with them.

A pointer holds the address of an object. int* p = &x; stores the address of x; *p dereferences it to reach x. For a pointer to a struct, p->member is shorthand for (*p).member.

Unlike references, pointers can be null (nullptr, never NULL or 0 in modern code), can be reassigned to point elsewhere, and need explicit dereferencing. Dereferencing a null or dangling pointer is undefined behaviour, so check for nullptr wherever null is a valid state.

Pointers and arrays are closely tied. An array name decays to a pointer to its first element, and pointer arithmetic moves by whole elements: p + 2 points two ints further, and p[i] means *(p + i). Arithmetic is only valid within one array (plus one past the end); going beyond is undefined.

const applies as usual, read right to left: const T* can't modify the pointee, T* const can't be repointed.

You can allocate with new and must then delete (or new[] with delete[]). Doing this by hand leaks memory on early returns and exceptions and invites double deletes. In modern C++ raw pointers don't own: use containers and smart pointers for ownership, and use raw pointers (or references) only to observe objects someone else owns.

So when should a function take T* instead of T&? When "no object" is a valid argument. Otherwise a reference states the intent better. Raw pointers remain essential for linked structures that point to optional neighbours, for C APIs, and for low-level code.

Example

#include <iostream>

struct Node { int value; Node* next; };   // can be null, can be repointed

int main() {
    int x = 10;
    int* p = &x;            // p holds the address of x
    *p = 20;                // write through the pointer
    std::cout << x << ' ' << (p != nullptr) << '\n';   // 20 1

    int arr[] = {1, 2, 3, 4};
    int* it = arr;          // array decays to a pointer to its first element
    it += 2;                // pointer arithmetic: now at arr[2]
    std::cout << *it << ' ' << it[1] << '\n';          // 3 4

    const int* read_only = &x;   // can't modify *read_only, can repoint
    int* const fixed = &x;       // can modify *fixed, can't repoint
    *fixed = 30;
    std::cout << *read_only << '\n';                   // 30

    Node c{3, nullptr}, b{2, &c}, a{1, &b};
    for (Node* n = &a; n != nullptr; n = n->next)      // n->value is (*n).value
        std::cout << n->value << ' ';                  // 1 2 3
    std::cout << '\n';
}

Remember

  • &x takes an address, *p dereferences, p->m accesses a member
  • Use nullptr; dereferencing null or a dangling pointer is undefined behaviour
  • Raw pointers observe; containers and smart pointers own

Watch out for

  • Using new/delete by hand in application code, leaking on early return or exception.

Check yourself

  1. Given `int a[4] = {10, 20, 30, 40}; int* p = a + 1;`, what is `p[2]`?
  2. When is a raw `T*` parameter more appropriate than `T&`?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: en.cppreference.com/w/cpp/language/pointer