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:

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 pointerint **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:

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

Further reading

Sources & References

Pointers, dereference, and arithmetic