Skip to content

1. Pointers & References

What & why

Every object lives somewhere in memory, at an address. A pointer is a variable that stores an address; a reference is another name for an existing object. Modern C++ uses raw pointers far less than old C++ did, but you can't read real code, use C libraries, or understand smart pointers without them.

Addresses

#include <iostream>

int main() {
    int x = 42;
    std::cout << x << '\n';      // 42 — the value
    std::cout << &x << '\n';     // 0x7ffd5c3e1a4c — the address (varies per run)
}

&x means "address of x".

Pointers

int x = 42;
int* p = &x;       // p is a "pointer to int" holding x's address

std::cout << p << '\n';    // the address
std::cout << *p << '\n';   // 42 — "dereference": the object p points at

*p = 100;                  // writes through the pointer
std::cout << x << '\n';    // 100

Two meanings of each symbol — context decides:

In a declaration In an expression
int* p — p is a pointer to int *p — the object p points to
int& r = x — r is a reference to x &x — the address of x

nullptr

A pointer that points at nothing should be nullptr:

int* p = nullptr;

if (p != nullptr) {        // or simply: if (p)
    std::cout << *p;
}

Dereferencing nullptr is undefined behavior — usually a segfault. Never use NULL or 0 for pointers in modern C++.

Pointers to class members: ->

struct Point { int x; int y; };

Point pt{3, 4};
Point* pp = &pt;

(*pp).x = 10;    // clumsy
pp->x = 10;      // same thing: -> dereferences and accesses a member

References

A reference is an alias — it must be initialized, can't be null, and can't be re-seated to refer to something else:

int a = 1, b = 2;
int& r = a;     // r IS a
r = b;          // this assigns b's VALUE to a (a == 2). It does NOT rebind r.

Pointer vs. reference

Pointer T* Reference T&
Can be null yes no
Can be re-pointed yes no
Must be initialized no (but should be) yes
Syntax to use *p, p->m just r, r.m
Typical use optional/re-seatable "handle", C APIs, arrays function parameters, aliases

Rule: use a reference when "nothing" isn't a valid value; use a pointer when it is.

const and pointers

Read the declaration right to left:

int x = 1, y = 2;

const int* p1 = &x;    // pointer to const int: can't change *p1, can re-point p1
*p1 = 5;               // ERROR
p1 = &y;               // OK

int* const p2 = &x;    // const pointer to int: can change *p2, can't re-point
*p2 = 5;               // OK
p2 = &y;               // ERROR

const int* const p3 = &x;   // neither

And for parameters:

void print(const std::vector<int>& v);     // promises not to modify v
void fill(std::vector<int>& v);            // will modify v
void maybe(const Widget* w);               // may be null, won't modify

This is const correctness: mark everything const that doesn't need to change. It documents intent, and the compiler enforces it.

Pointers and arrays

A C array's name converts ("decays") to a pointer to its first element, and you can do arithmetic on pointers:

int arr[4] = {10, 20, 30, 40};
int* p = arr;          // &arr[0]

*(p + 2)               // 30 — same as arr[2] and p[2]
p++;                   // now points to arr[1]
std::cout << *p;       // 20

Pointer arithmetic is only valid within one array (and one past its end). Going outside is undefined behavior. Modern code uses std::span (C++20) instead of pointer + length:

#include <span>

int sum(std::span<const int> values) {    // accepts vector, array, C array...
    int total = 0;
    for (int v : values) total += v;
    return total;
}

std::vector<int> v{1, 2, 3};
std::array<int, 2> a{4, 5};
sum(v);
sum(a);

Dangling pointers and references

A pointer/reference to an object that no longer exists is dangling. Using it is undefined behavior:

int* make_bad() {
    int local = 5;
    return &local;          // local dies here — returned pointer dangles
}

std::vector<int> v{1, 2, 3};
int& first = v[0];
v.push_back(4);             // may reallocate → first now dangles
std::cout << first;         // UB

The compiler warns about some of these (-Wall); AddressSanitizer (-fsanitize=address) catches most at runtime. Use it.

Function pointers (briefly)

int add(int a, int b) { return a + b; }

int (*op)(int, int) = &add;   // pointer to a function taking (int, int) returning int
op(2, 3);                     // 5

You'll mostly use lambdas and std::function instead (lesson 9).

Common mistakes

  • Uninitialized pointers — int* p; holds garbage. Initialize to nullptr or a real address.
  • Dereferencing nullptr.
  • Dangling pointers/references to locals, to temporaries, or into containers that reallocated.
  • int* a, b; — only a is a pointer; b is an int. Declare one per line.
  • Confusing r = b with rebinding a reference.
  • Pointer arithmetic outside an array.
  • Returning const T& to a member of a temporary.

Exercises

  1. Write void swap_ptr(int* a, int* b) and void swap_ref(int& a, int& b). Call both. Which call site is clearer?
  2. Write int* find_first_negative(std::span<int> values) that returns nullptr if none, and use it to set the first negative number to zero.
  3. Print the addresses of consecutive elements of a std::vector<int> and of a std::vector<double>. What's the difference between neighbors? Why?
  4. For each declaration, say what can and can't be modified: const char* s, char* const s, const std::string& s, std::string* const* pp.
  5. Write the "dangling push_back" bug on purpose and run it under -fsanitize=address -g. Read the report — which line allocated, which freed, which used?

Recap

  • &x takes an address; T* p stores one; *p dereferences; p->m accesses a member.
  • nullptr means "points at nothing" — check before dereferencing.
  • References are non-null, non-reseatable aliases — the default for parameters.
  • Read const right to left; be const-correct everywhere.
  • Prefer std::span over pointer + length.
  • Dangling pointers/references are the #1 C++ memory bug — sanitizers find them.

Next: Memory: the stack, the heap & RAII →