🎯 Pointers & Memory
malloc & friends: memory on demand
▶ Open the interactive lesson — free, no signupThat browser that gets slower and slower until you restart it? Odds are it's leaking memory — the signature failure of the by-hand memory management you're about to learn. malloc is how programs handle data whose size nobody knows until a user shows up with it: their file, their message, their playlist. Every array you've built so far had its size baked in at compile time; after this lesson, that restriction is gone.
Every array so far had a size fixed at compile time (or lived dangerously on the stack as a VLA). But real programs read files, take input, grow lists — they need memory whose size is known only at runtime, and which can outlive the function that created it. That memory lives on the heap, and you manage it by hand.
The core pair: malloc and free
malloc(n) (from <stdlib.h>) requests n bytes from the heap and returns a pointer to them — or NULL if the system can't oblige. The bytes are uninitialized garbage. When you're done, and not before, you return them with free(p):
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int n = 5; /* runtime size — fine! */
int *a = malloc(n * sizeof *a);
if (a == NULL) { /* malloc CAN fail */
fprintf(stderr, "out of memory\n");
return 1;
}
for (int i = 0; i < n; i++) /* contents were garbage */
a[i] = i * i;
printf("a[4] = %d\n", a[4]);
free(a); /* give it back */
a = NULL; /* defuse the dangler */
return 0;
}$ gcc firstmalloc.c -o firstmalloc && ./firstmalloc a[4] = 16
The sizeof *a idiom: writing malloc(n * sizeof *a) instead of malloc(n * sizeof(int)) means the size expression is tied to the pointer itself — change a's type later and the allocation stays correct automatically. Also note: no cast on malloc's result needed in C (that's a C++ habit).
🧠 Checkpoint: Why prefer malloc(n * sizeof *a) over malloc(n * sizeof(int))?
- It allocates faster
- It stays correct automatically if a’s type ever changes
- sizeof(int) is deprecated
- The compiler requires it since C11
Show answer
It stays correct automatically if a’s type ever changes — sizeof *a means "the size of whatever a points to" — refactor int* to long* and the allocation follows along. The spelled-out type is a bug waiting for a refactor.
calloc and realloc
calloc(count, size) allocates and zeroes the memory (and checks that count × size doesn't overflow). realloc(p, newsize) resizes an allocation — possibly by moving it somewhere else entirely, copying your data over:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *a = calloc(4, sizeof *a); /* 4 ints, all ZERO */
if (!a) return 1;
printf("a[3] = %d (calloc zeroes)\n", a[3]);
int *bigger = realloc(a, 8 * sizeof *a);
if (bigger == NULL) { /* old block STILL ok */
free(a);
return 1;
}
a = bigger; /* may have MOVED! */
a[7] = 99; /* new bytes: garbage, */
printf("a[0] = %d, a[7] = %d\n", a[0], a[7]); /* not 0 */
free(a);
return 0;
}$ gcc grow.c -o grow && ./grow a[3] = 0 (calloc zeroes) a[0] = 0, a[7] = 99 # realloc kept bytes 0-15 intact, even if it moved the block
Never write p = realloc(p, n). If realloc fails it returns NULL but leaves the old block allocated — and you just overwrote your only pointer to it. That's a guaranteed leak. Always catch the result in a fresh variable, check it, then assign.
🧠 Checkpoint: What does calloc give you that malloc doesn’t?
- Stack allocation
- Zero-filled memory, plus an overflow check on count × size
- Automatic freeing at scope exit
- Faster allocation
Show answer
Zero-filled memory, plus an overflow check on count × size — calloc zeroes every byte and safely detects if count × size would overflow. malloc hands you uninitialized garbage — reading it before writing is UB.
The three deadly sins
Manual memory management has exactly three classic failure modes. Learn their names — you will meet all of them:
- Memory leak: losing the last pointer to a block without freeing it. The program's memory use grows forever; long-running servers die slowly.
- Use-after-free: dereferencing a pointer after
free. The allocator may have recycled those bytes for something else — you're reading or corrupting a stranger's data. - Double free: freeing the same pointer twice corrupts the allocator's own bookkeeping. Modern allocators often abort with
free(): double free detected.
int *p = malloc(sizeof *p);
*p = 42;
p = malloc(sizeof *p); /* SIN 1: leak — first block lost */
free(p);
printf("%d\n", *p); /* SIN 2: use-after-free — UB */
free(p); /* SIN 3: double free — UB, often */
/* aborts the program */▶ This spot has an interactive memgrid widget — open the interactive lesson to play with it.
free(p) does not change p. The pointer still holds the old address — now a dangling pointer. The pro habit: free(p); p = NULL;. Dereferencing NULL crashes loudly and immediately; dereferencing a dangling pointer corrupts quietly and ruins your week. (Bonus: free(NULL) is defined as a harmless no-op.)
🧠 Checkpoint: After free(p);, what is the state of p itself?
- It becomes NULL automatically
- It still holds the old address — a dangling pointer
- It’s deallocated along with the block
- Reading p (not *p) is a crash
Show answer
It still holds the old address — a dangling pointer — free receives a COPY of the address, so it can’t modify your variable. p dangles until you overwrite it — hence the discipline: free(p); p = NULL;
Your new best friend: valgrind
You don't have to hunt these bugs with printf. valgrind runs your program in an instrumented sandbox and reports every leak, use-after-free, and out-of-bounds access with a stack trace:
$ gcc -g leaky.c -o leaky && valgrind ./leaky ==7412== HEAP SUMMARY: ==7412== in use at exit: 40 bytes in 1 blocks ==7412== total heap usage: 2 allocs, 1 frees ==7412== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1 ==7412== at 0x4846828: malloc (vg_replace_malloc.c:446) ==7412== by 0x109172: main (leaky.c:6) # "definitely lost" = a leak, with the exact line that allocated it
We'll tour valgrind and AddressSanitizer properly in the toolchain part — for now, just know that valgrind ./myprog is one command and it catches what your eyes can't.
▶ This spot has an interactive editor widget — open the interactive lesson to play with it.
So far the heap has held plain arrays — next we'll teach C to allocate records with named fields: structs.
▶ Practice this lesson interactively (with live gcc)