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:
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 tonullptror a real address. - Dereferencing
nullptr. - Dangling pointers/references to locals, to temporaries, or into containers that reallocated.
int* a, b;— onlyais a pointer;bis anint. Declare one per line.- Confusing
r = bwith rebinding a reference. - Pointer arithmetic outside an array.
- Returning
const T&to a member of a temporary.
Exercises¶
- Write
void swap_ptr(int* a, int* b)andvoid swap_ref(int& a, int& b). Call both. Which call site is clearer? - Write
int* find_first_negative(std::span<int> values)that returnsnullptrif none, and use it to set the first negative number to zero. - Print the addresses of consecutive elements of a
std::vector<int>and of astd::vector<double>. What's the difference between neighbors? Why? - 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. - 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¶
&xtakes an address;T* pstores one;*pdereferences;p->maccesses a member.nullptrmeans "points at nothing" — check before dereferencing.- References are non-null, non-reseatable aliases — the default for parameters.
- Read
constright to left; be const-correct everywhere. - Prefer
std::spanover pointer + length. - Dangling pointers/references are the #1 C++ memory bug — sanitizers find them.