Pointers
Pointers are the heart of C, the feature that makes it powerful and the one that makes it dangerous. A pointer is just a variable that holds a memory address — but that simple idea is how C functions modify their callers, how arrays and strings work, how dynamic memory is managed, and how data structures are built. Everything hard and everything essential in C runs through pointers.
The previous post ended with a cliffhanger: because C passes arguments by value, you need a way to hand a function the address of something so it can modify the original. That way is pointers. This is the pivotal post of the series — pointers underpin arrays, strings, dynamic memory, and data structures in every post that follows. We’ll build the concept carefully, because getting the model right here makes the rest of C click.
A pointer is an address
Every variable lives somewhere in memory, at a numbered location called its address. A pointer is a variable whose value is an address — it “points to” another variable by holding where that variable lives. Two operators connect values and addresses:
&(address-of) — gives you the address of a variable.*(dereference) — given a pointer, gives you the value at that address.
int x = 42;
int *p = &x; // p holds the address of x; p "points to" x
printf("%d\n", x); // 42 — the value
printf("%p\n", (void*)p); // some address, e.g. 0x7ffe...
printf("%d\n", *p); // 42 — dereference: the value AT that address
*p = 99; // write THROUGH the pointer to what it points to
printf("%d\n", x); // 99 — x changed, via p
The declaration int *p reads as “p is a pointer to an int.” The type matters: a pointer knows what kind of thing it points to, which determines how dereferencing interprets the bytes and how pointer arithmetic (below) steps. *p = 99 is the key move: it reaches through the pointer and modifies the variable p points to. That’s how a pointer lets you affect something elsewhere.
Pointers solve pass-by-value
Now the payoff from post 3. To let a function modify a caller’s variable, pass the variable’s address, and have the function write through the pointer:
void increment(int *n) {
*n += 1; // modify what n points to — the caller's variable
}
int main(void) {
int count = 5;
increment(&count); // pass the ADDRESS of count
printf("%d\n", count); // 6 — the original was modified
return 0;
}
increment still receives its argument by value — but the value is an address, a copy of which points to the same count. Dereferencing it reaches the original. This is the universal C pattern for “output parameters” and for functions that must change caller state. It’s also how scanf works (scanf("%d", &x) passes the address so it can store the input into your variable). Once you see pass-by-value + pointers, most of C’s function conventions make sense.
The null pointer
A pointer that points to nothing is set to NULL (a special zero-valued address meaning “not pointing at anything”):
int *p = NULL;
if (p != NULL) {
*p = 5; // safe: only dereference after checking
}
NULL is how C represents “no valid target” — an uninitialized-yet, not-found, or end-of-list pointer. The cardinal rule: never dereference a null (or invalid) pointer. Doing so is undefined behavior, typically a segmentation fault that crashes your program. Checking for NULL before dereferencing is a constant, necessary habit — functions that might fail to produce a pointer (like memory allocation, post 6) return NULL on failure, and you must check. Equally dangerous is the uninitialized pointer: int *p; then *p = 5; writes through a garbage address — always initialize a pointer, to NULL if you have nothing yet.
Pointer arithmetic
Pointers support arithmetic, and C makes it type-aware: adding 1 to a pointer advances it by the size of the type it points to, not by one byte:
int arr[4] = {10, 20, 30, 40};
int *p = arr; // points to arr[0]
printf("%d\n", *p); // 10
printf("%d\n", *(p+1)); // 20 — p+1 advances by sizeof(int), to the next element
p + 1 doesn’t add 1 to the raw address; it adds sizeof(int) (typically 4 bytes), landing exactly on the next int. This type-aware stepping is what makes pointer arithmetic useful for walking through arrays — and it’s the deep reason arrays and pointers are so tightly linked, which the next post explores fully. Pointer arithmetic is powerful and also a prime source of bugs: step past the end of your data and you’re reading or writing memory you don’t own (undefined behavior). The arithmetic itself is only valid within an array (and one-past-the-end); beyond that, all bets are off.
Pointers to pointers
Because a pointer is itself a variable with an address, you can have a pointer to a pointer — int **pp — which holds the address of an int *. This seems abstract but is genuinely useful:
int x = 42;
int *p = &x;
int **pp = &p; // pp points to p
printf("%d\n", **pp); // 42 — dereference twice: pp -> p -> x
Two dereferences (**pp) walk two hops: pp gives you p, and *p gives you x. You need this whenever a function must modify a pointer in the caller — for example, a function that allocates memory and stores the resulting pointer into the caller’s variable takes an int ** (the address of the caller’s int *), exactly as increment took an int * to modify a caller’s int. It’s the same pass-by-value logic, one level up. Pointer-to-pointer also underlies arrays of strings (char **argv, the argument list to main) and dynamic arrays of pointers.
Why pointers are the heart of C
Step back and see how much pointers unlock — every remaining topic in this series is a pointer application:
- Modifying caller state (this post) — pass an address, write through it.
- Arrays and strings (post 5) — an array name is effectively a pointer to its first element; C strings are pointers to characters.
- Dynamic memory (post 6) —
mallocreturns a pointer to heap memory you own and must free. - Data structures (post 7) — linked lists, trees, and graphs are built from structs that point to other structs.
This is why pointers are worth the effort to truly understand rather than memorize. They’re not a feature bolted onto C; they’re the mechanism through which C does almost everything interesting. The cost is danger — null dereferences, dangling pointers, out-of-bounds arithmetic, all of which crash or corrupt — so C programming is, in large part, the discipline of using pointers correctly. Get the model solid (a pointer holds an address; * reaches the value; the type controls interpretation and stepping) and the rest of the language becomes learnable.
Key takeaways
- A pointer is a variable holding a memory address;
&xgives a variable’s address and*pdereferences (reads/writes the value at that address), so*p = 99modifies whatppoints to. - Pointers solve pass-by-value: pass a variable’s address (
&count) and a function writing through the pointer (*n += 1) modifies the caller’s original — the universal pattern for output parameters (and howscanfstores input). - Never dereference a
NULLor uninitialized pointer (undefined behavior / segfault) — check forNULLbefore use (allocation and lookups returnNULLon failure), and always initialize pointers. - Pointer arithmetic is type-aware:
p + 1advances bysizeof(*p), landing on the next element — powerful for walking arrays but a bug source if you step outside the valid range. - A pointer to a pointer (
int **) holds the address of a pointer — needed to modify a caller’s pointer (e.g. an allocator storing into the caller’s variable) and underlyingchar **argvand dynamic pointer arrays. - Pointers are the mechanism behind nearly everything in C — caller modification, arrays/strings, dynamic memory, data structures — so mastering the model (address / dereference / type-aware) is mastering C, and the discipline of using them safely is much of the language.