The Pointer,
visually.
A pointer is not magic. It is a small variable — eight bytes, on most machines — whose only job is to hold an address. Watch one come to life, move, dereference, and (occasionally) misbehave. No abstractions, no metaphors. Just memory.
What it actually is.
A box that holds a number. The number is an address.
Every variable in your program lives at some address in memory. int x = 42;
might place x at 0x1000. A pointer to x — written
int *p = &x; — is itself a variable, living at its own address (say
0xFF20), and the value stored inside it is 0x1000.
That's the entire idea. Two boxes, one holding 42, the other holding the address of the first.
A pointer is just a box that holds an address.
Hover the highlighted parts to see what each piece is.
&x
Reads "address of x." Returns the location where x lives.
Type is int* if x is an int. This is how you put a real address
into a pointer — there's no other way.
*p
Reads "the thing p points to." Goes to the address stored in p,
loads whatever is there. *p on the left side writes;
*p on the right side reads.
Dereferencing, animated.
Watch the pointer fetch a value, then write a new one.
The two operations every pointer ever performs: read through (load the value at the address it holds) and write through (store a new value at that address). Both follow the arrow.
Read and write, through the arrow.
Click the buttons to perform each operation.
Pointer arithmetic.
When p++ moves four bytes, not one.
Adding 1 to a pointer doesn't add 1 byte — it adds one element-size.
For an int* on a typical machine, that's 4 bytes. For a double*,
8 bytes. The compiler knows the element type and silently scales the offset. This is
why arr[i] and *(arr + i) land on the same cell:
arr + i means "advance i × sizeof(element) bytes."
Move the pointer along an int array.
Each p++ jumps four bytes — exactly one int.
Pointer to pointer.
Indirection, taken one level further.
If a pointer holds the address of an int, then a pointer to a pointer
holds the address of an int-pointer. Written int **pp. Three boxes,
two arrows. Used everywhere: when a function needs to change a pointer the
caller passed in (think argv, or how realloc lets you
swap a buffer), or to build a 2D array of strings.
Two arrows. One value.
Click each step to see what **pp resolves to.
When pointers lie.
NULL, dangling, wild — three kinds of trouble.
A pointer is just bytes. Nothing in those bytes guarantees they form a valid address. Three failure modes show up again and again, and recognizing them on sight is half the battle.
Three ways pointers go wrong.
Click each kind to see what's actually in memory.
Five lines, memorized.
If these read fluently, you understand pointers.
Once these stop looking strange, everything in C/C++ that involves pointers is just composition of these five primitives.
int *p;
Reserves an 8-byte slot to hold an address. Currently holds garbage — declaring a pointer does not initialize it. Always assign before dereferencing.
p = &x;
Stores the address of x into p. Now p
and &x hold the same value. The arrow is drawn.
int v = *p;
Follows the arrow. Loads whatever int lives at the address in p
and copies it into v. x itself is untouched.
*p = 99;
Follows the arrow. Stores 99 at the address in p — which means
x is now 99. The original variable changed,
through the pointer.
p++;
Adds sizeof(*p) bytes to p. For int*, four bytes.
For double*, eight. The compiler scales the step by the element size,
which is why arrays and pointers feel interchangeable when you write arr[i]
— that's just *(arr + i) with the scaling already done.