Pointers scared me for an embarrassingly long time. The syntax did not help: asterisks meaning two different things, ampersands, arrows. But the concept is simple once you strip the syntax away. A pointer is a variable whose value is a memory address. That is it. It is a number that tells you where something lives.
Memory is one giant array
Picture your process’s address space as one enormous array of bytes, indexed from zero up to some huge number. Every variable you declare lives at some index in that array. A pointer just stores one of those indices. When you “dereference” a pointer, you are saying “go to that index and read what is there.”
int x = 42;
int *p = &x; // p holds the address of x
printf("%d\n", *p); // dereference: prints 42
*p = 7; // write through the pointer
printf("%d\n", x); // prints 7
The ampersand means “address of” and the asterisk in an expression means “the thing at this address.” The same asterisk in a declaration means “this variable is a pointer.” Two different jobs for one symbol, which is the main reason pointers look confusing at first.
The type matters more than you think
A pointer is not just an address, it is an address plus a type. The type tells the compiler two things: how many bytes to read when you dereference, and how far to jump when you do arithmetic. An int pointer and a char pointer can hold the exact same numeric address and behave completely differently.
int arr[4] = {10, 20, 30, 40};
int *p = arr; // points at arr[0]
p++; // now points at arr[1], moved 4 bytes not 1
printf("%d\n", *p); // prints 20
That is the key insight about pointer arithmetic. Adding one to an int pointer moves it by sizeof(int) bytes, not by one byte. The compiler scales for you based on the type. This is also why arrays and pointers feel so interchangeable in C: indexing arr[i] is defined as taking the address of arr, adding i times the element size, and dereferencing.
Where pointers point
A pointer can hold the address of a stack variable, a heap allocation, a function, or nothing at all. The where matters because it decides whether dereferencing is safe. If you are fuzzy on stack versus heap lifetimes, that is the foundation here, and I covered it in stack vs heap: how memory actually works. A pointer to a stack variable becomes dangling the moment that frame returns. A pointer to freed heap memory is a use-after-free waiting to crash.
- NULL pointer: holds address zero, a deliberate “points at nothing.” Dereferencing it crashes, which is actually the friendly outcome.
- Dangling pointer: holds an address that used to be valid. Dereferencing it is undefined behavior and may silently corrupt data.
- Wild pointer: never initialized, holds garbage. The worst kind because it can point anywhere.
Pointers to pointers
Once a pointer is just a variable, a pointer to a pointer stops being mysterious. It is the address of a variable that itself holds an address. You need this whenever a function must change where a pointer points, not just what it points to.
void allocate(int **out) {
*out = malloc(sizeof(int)); // write a new address into the caller's pointer
**out = 99; // write a value into that memory
}
int *p = NULL;
allocate(&p); // pass the address of p so the function can modify it
printf("%d\n", *p); // prints 99
const and what is really protected
One thing that trips people up is where const sits relative to the asterisk, because a pointer has two things that can be constant: the address it holds, and the data it points at. A const int pointer means you cannot change the data through it, but you can repoint it. A pointer that is itself const means you can change the data but not where it points. Reading the declaration right to left helps. This matters because the compiler will enforce it, and getting it wrong produces error messages that look more confusing than the underlying idea.
Why this is worth the trouble
Pointers are the mechanism behind almost everything interesting in systems code: linked structures, dynamic memory, passing large objects without copying them, talking to hardware at fixed addresses. They are also the source of most crashes in C. The allocator I describe in writing a simple memory allocator is nothing but careful pointer manipulation over a raw block of bytes. Once you see a pointer as a typed integer into the big byte array, the fear goes away and the power shows up.