Week 17 · Phase 2 — The Ancestry
Pointers and memory addresses — where C reveals what's been happening all along.
Photo · Oliver Potter / Unsplash
This is the chapter that, traditionally, breaks beginners. It is also the chapter that, once it clicks, makes every subsequent chapter dramatically simpler. In Python, JavaScript, Java, Swift, when you say x = some_object, the language is doing something behind your back — and that something has a name. C makes you do it explicitly. Once you've done it explicitly, you can never again be confused about what was happening before.
Pointers are not hard. Pointers are surprising — for about an hour. Then, like a Magic Eye picture, the depth resolves and stays resolved. Today is that hour.
RAM is one giant array of bytes, indexed from 0. Every variable lives at some byte position. That byte position is called the variable's address. A pointer is a variable whose value is an address.
That's the whole concept. The rest is bookkeeping.
You can take the address of any variable with the & operator. You can store that address in a pointer variable. You can later look up what's at that address using the * operator (called "dereferencing"). C lets you talk about the location and the contents separately, and gives you operators for going between them.
int x = 42; // x is an int. Lives somewhere in memory.
int *p = &x; // p is a pointer to an int. Holds x's address.
printf("x is %d\n", x); // 42
printf("&x is %p\n", (void*)&x); // e.g. 0x16d3a3c2c
printf("p is %p\n", (void*)p); // same address as &x
printf("*p is %d\n", *p); // 42
*p = 99; // write 99 to whatever p points at...
printf("x is now %d\n", x); // ...and so x is now 99
Read those last two lines slowly. You wrote to *p. You did not name x. But x changed. That's because p contained x's address — so writing through p reached the same byte that x's name reaches. The pointer is a remote control for the variable.
That's the entire picture. A pointer holds an address. &x means the address of x. *p means the value at the address that p contains. Two operators, perfectly inverse: *&x == x, always.
Three reasons, each of which is a feature you cannot have without them.
// Without pointers — does NOT change x:
void add_one(int n) { n++; } // modifies a copy
// With pointers — DOES change x:
void add_one_p(int *np) { (*np)++; } // modifies what np points at
int x = 5;
add_one(x); // x is still 5
add_one_p(&x); // x is now 6
C passes function arguments by value — the function gets a copy. To let a function modify its caller's variable, you pass the address instead. The function's parameter is a pointer; dereferencing it reaches back to the original. This is how scanf("%d", &x) works — you give it the address of x so it can write into it.
An array name in C is just a pointer to the first element. arr[3] is mathematically equivalent to *(arr + 3) — take arr's base address, add 3 element-widths, dereference. Iterating an array with a pointer is, secretly, exactly what every for (i = 0; i < n; i++) loop does:
int arr[5] = {10, 20, 30, 40, 50};
// these two are equivalent:
for (int i = 0; i < 5; i++) printf("%d\n", arr[i]);
for (int *p = arr; p < arr + 5; p++) printf("%d\n", *p);
Same machine code. Walking pointers across memory is what arrays are, under the hood. This is also why "passing an array to a function" in C silently passes a pointer — the array doesn't get copied, only its base address.
Stack variables only live as long as their function. To make data that outlives a function — say, a buffer for a file you'll read into, or a tree node you'll insert into a data structure — you ask the OS for a chunk of memory at runtime. That chunk lives on the heap. The function that asks gets back, naturally, a pointer to it:
int *numbers = malloc(100 * sizeof(int));
// use numbers[0]..numbers[99]
free(numbers); // hand it back when done
malloc returns the address of 400 fresh bytes. You use them. You must later free them, or those bytes are leaked — gone, unreachable, until the program exits. Forgetting to free is the canonical C "memory leak". Freeing twice, or using memory after freeing, is the canonical C crash. The whole reason garbage-collected languages exist (Java, Python, Go) is to do this for you. The whole reason Rust exists is to do this for you without a garbage collector.
A pointer doesn't have to point at anything valid. It can be null — the special "points at nothing" value, written as NULL in C (it's just the address 0).
Dereferencing a null pointer — saying *p when p is null — is a fatal crime. The OS catches the attempt, kills your program, and prints the famously terse message: Segmentation fault. Or, if you're lucky enough to be on Windows: Access violation. The chef tried to read from address 0, which is in a region the OS reserves to catch exactly this mistake.
The same crash also happens if you dereference a "wild" pointer — one that was never initialised, or freed, or off the end of an array. C does not protect you. Higher-level languages do — at runtime cost, with checks the chef has to perform on every dereference.
The defence is straightforward: always check for NULL before dereferencing, especially after malloc (which returns NULL on out-of-memory).
In Python:
a = [1, 2, 3]
b = a # reference, not copy
b.append(4)
print(a) # [1, 2, 3, 4] — surprised?
This surprises every Python beginner. b = a didn't copy the list. It made b point at the same list that a points at. In C terms: a and b are both pointers to the same heap-allocated thing. There is no way to express this idea cleanly in a language that pretends pointers don't exist — but the behaviour is unavoidable, because copying a million-element list every time you assign would be fatally slow.
Once you have C's mental model — "names are addresses, assignment copies addresses, mutation goes through to the underlying object" — Python, JavaScript, Swift, Java, Ruby all stop containing surprises. You'll know exactly what's happening. The other languages were doing the same thing all along; C just made you say it out loud.
Tensors in PyTorch, NumPy arrays, CUDA buffers — these are, internally, big chunks of heap memory plus metadata about their shape. You write y = x in Python, and you've copied a pointer. Slice x[:, 0] and you've made a new "view" — another pointer into the same underlying memory, with a different stride. Modify the slice and you modify the original. This trips beginners constantly. C-trained programmers see it coming a mile away.
And every kernel that runs on a GPU is, ultimately, given device pointers — addresses in GPU memory — and asked to do work. The tensors don't move; the pointer is what's passed across the language boundary. Knowing what a pointer is means you can read the C++ side of any AI framework's source code and not be lost.
Pointers don't make C scary. They make C honest. Every other language is using them — they just don't tell you.
int, make a pointer to it, modify the int through the pointer, print both. Watch the variable change without ever using its name on the assignment line.swap(int *a, int *b) that swaps the values of two ints. Test it. Now try writing the same thing without pointers and see why it can't be done.malloc(10 * sizeof(int)). Fill it. Print it. free it. Now run your program with valgrind ./your_program on Linux, or under leaks --atExit -- ./your_program on macOS. The tool will tell you if you leaked anything — and exactly where.main, and the address of a local variable in a function it calls. They'll be close to each other but different. You're seeing the stack in action.Phase 2 is now closed. You have C — variables, control flow, loops, pointers, the whole basic vocabulary the chef speaks. From here on, every language and library will feel familiar in a way it didn't a month ago.
But "vocabulary" isn't enough to build big programs. You also need organisation. Phase 3 is the leap that took C from "good for systems" to "good for everything else". Bell Labs again, slightly later, slightly down the corridor.
Week 18 is C with Classes — Bjarne Stroustrup, C++, and the idea that changed how every large piece of software has been organised since 1985.
Photo free under the Unsplash license. Signs · Oliver Potter.