C
Pointers
A pointer is a variable whose value is a memory address — not a number you compute with, but a location you can follow to reach the actual value stored there. Pointers are the single most feared topic in beginner C, almost always because of notation, not concept — the underlying idea is simple: "the address of a thing" and "the thing at an address" are two different, related ideas, and C gives you an operator for each.
- What it is
- A variable that stores a memory address instead of an ordinary value.
- Mental model
- A sticky note with someone else's house address written on it — the note itself isn't the house, but it tells you exactly where to go to find it.
- Key numbers
- A pointer's own size is fixed by the platform (commonly 4 or 8 bytes) regardless of what type it points to — a pointer to a
charand a pointer to adoubletake the same space. - Reach for it when
- A function needs to modify a caller's variable, you need to refer to one piece of data from multiple places without copying it, or you're working with arrays/strings (which are built directly on this idea).
The address-of operator: &
Every variable lives somewhere in memory. The & operator, placed before a variable's
name, gives you that variable's address instead of its value:
int age = 25;
printf("%d\n", age); // 25 — the VALUE
printf("%p\n", (void*)&age); // e.g. 0x7ffee2a1 — the ADDRESS
Declaring and dereferencing a pointer
A pointer variable is declared with a * before its name, and its type says what kind of
value it points to:
int age = 25;
int *p = &age; // p is a pointer to an int, holding age's address
printf("%d\n", *p); // 25 — dereferencing: "the value AT this address"
*p = 30; // changes age itself, through the pointer
printf("%d\n", age); // 30
p is itself an ordinary variable, with its own address (0x2000) — the only thing special about it is that the value stored inside it is interpreted as an address pointing somewhere else.
Read int *p as "*p is an int" — the declaration describes what
you get after dereferencing, which is why the pointer's declared type has to match what it
points to.
Passing by reference
Recall from Functions that C passes arguments by value — a function only ever gets a copy. Passing a pointer to a variable lets a function reach back and modify the original, because the address it receives points at the real thing:
void increment(int *x) {
*x = *x + 1; // modifies whatever x points at
}
int main(void) {
int n = 5;
increment(&n); // pass n's ADDRESS, not n itself
printf("%d\n", n); // 6 — n was actually changed
return 0;
}
This is the single most common reason to reach for a pointer as a beginner: it's how a function returns more than one value, or modifies something the caller owns.
NULL
NULL is a special pointer value meaning "points at nothing valid" — the conventional way to
represent "no address yet" or "this pointer isn't set up." Dereferencing a NULL pointer
(trying to read or write "the value at nothing") is undefined behavior, and on most desktop operating
systems crashes immediately (a "segmentation fault") — which is actually the friendly outcome
compared to what can happen on hardware with no memory protection at all.
int *p = NULL;
if (p != NULL) {
printf("%d\n", *p); // only dereference after checking
}
Pointer arithmetic, briefly
Adding to a pointer moves it forward by that many elements of its pointed-to type, not that
many bytes — p + 1 on an int* moves 4 bytes ahead (one whole int),
not 1 byte:
int scores[3] = {10, 20, 30};
int *p = scores; // points at scores[0] — recall array decay from Arrays
printf("%d\n", *p); // 10
printf("%d\n", *(p + 1)); // 20 — one int forward, not one byte forward
This is exactly the mechanism Arrays relies on for indexing — this section is intentionally brief; the full depth (arithmetic on other pointer types, subtracting two pointers, common bugs) is covered properly in Embedded C's Pointers, worth reading once you're comfortable here.
Real-world applications
- Modifying caller state — the
incrementpattern above is used constantly, including by standard library functions likescanf("%d", &n), which needs a pointer to actually write inton. - Passing large data efficiently — passing a pointer to a large struct or array avoids copying it entirely, just a handful of bytes for the address itself.
- Hardware register access — memory-mapped I/O on a microcontroller is, underneath, always a pointer to a fixed hardware address — the foundation of Embedded C's Registers & volatile.
Gotchas
- An uninitialized pointer (declared but never assigned an address) contains garbage — dereferencing it reads or writes a random, unpredictable memory location. Always initialize a pointer, even if only to
NULL. - Confusing
*in a declaration (int *p;, "declare a pointer") with*in an expression (*p = 5;, "dereference") is the single most common source of beginner confusion — the same symbol means two related but different things depending on context. - A dangling pointer — one still holding the address of something that no longer exists, like a local variable after its function returned — looks identical to a valid pointer right up until it's dereferenced. See Functions' call stack section for exactly why a local's storage disappears on return.
- Forgetting the
&when a function needs to modify your variable (passingninstead of&n) is a very common, easy-to-miss mistake — the code often still compiles, sincenis a validint, it just silently doesn't do what you meant.