Why the stack is not enough
Every variable you have declared so far has had its lifetime decided for you. A local `int x` is born when its function is called and dies the instant that function returns — it lives in a stack frame that is pushed and popped automatically. That is wonderfully convenient, but it hides a hard limit: the stack can only give you memory whose size is known at compile time and whose life ends with the call. What if you do not yet know how big an array needs to be — say, the number of lines in a file you have not read yet? What if a value must outlive the function that creates it, returning a result the caller keeps for hours?
For exactly these cases there is a second region of memory called the heap, and the act of asking it for storage is dynamic memory allocation. The word dynamic is the whole point: unlike a stack frame, whose size and lifetime are fixed by the structure of your code, a heap block's size is chosen while the program runs and its lifetime is whatever you decide. The cost of that freedom is responsibility — the heap will not clean up after you. Every byte you borrow, you must eventually return by hand. The rest of this rung is, in a sense, about doing that correctly.
malloc: asking for a block
The workhorse request is malloc() — short for memory allocate. You call it with a single argument, the number of bytes you want, and it returns a pointer to the start of a fresh block at least that large: `void *malloc(size_t n)`. The argument's type, `size_t`, is an unsigned integer wide enough to express any object size on your machine, which is why you size the request with `sizeof`. Want room for 100 ints? Ask for `malloc(100 * sizeof(int))`, never `malloc(100)` — the latter gives you 100 bytes, only 25 ints on a typical machine.
Notice the return type: `void *`, a typeless pointer that means "the address of some bytes, of no particular kind". malloc() deals in raw memory and has no idea what you will store there, so it hands back the most neutral pointer there is. You give it a type by assigning it to a typed pointer — `int *p = malloc(...)` — after which `p[0]`, `p[1]` and friends index it as ints. In C this conversion is automatic and you should not cast it; an explicit `(int *)` cast is unnecessary and can even hide the bug of a forgotten `#include`.
There is one promise malloc() pointedly does not make: the bytes it returns are uninitialised. They hold whatever leftover values were last in that memory — not zero, not anything you can predict. Reading them before you write them is undefined behaviour, the kind of bug that may look fine at `-O0` and corrupt silently at `-O2`. So the rhythm is always: allocate, then initialise, then use. If you want the block pre-zeroed, the next section has a call that does it for you.
The family: calloc and realloc
malloc() has two relatives that handle common needs more safely. calloc() — clear allocate — takes two arguments, a count and an element size, as in `calloc(100, sizeof(int))`, and gives you room for 100 ints with every byte set to zero. The two-argument form is not just sugar: calloc() checks internally that `count * size` does not overflow, so it is the safer way to size an array whose length came from outside your program. Use it whenever you want a zeroed block, which for arrays of numbers or pointers is often exactly what you want.
The third call, realloc(), resizes a block you already hold: `realloc(p, new_size)`. This is how a growing array doubles its capacity — you keep your old data, ask for more room, and realloc() either stretches the block in place or quietly moves it to a roomier spot and copies your bytes across. That last possibility carries a sharp trap: realloc() may return a different address, so the old pointer `p` may now be dangling. You must capture the return value, and you must do it into a temporary, because if realloc() fails it returns `NULL` while leaving the original block untouched — assigning straight back to `p` would lose your only handle to it and leak the memory.
/* grow an int array from n to n*2 elements, safely */
size_t new_n = n * 2;
int *tmp = realloc(p, new_n * sizeof(int));
if (tmp == NULL) { /* realloc failed: p is still valid */
perror("realloc");
free(p); /* clean up the block we still own */
return -1;
}
p = tmp; /* success: adopt the (maybe new) address */
n = new_n;free: giving it back
Borrowing implies returning. When you are done with a heap block, you hand it back with free(), passing the exact pointer that malloc(), calloc() or realloc() gave you: `free(p)`. This does not erase the memory or change the variable `p` — it simply tells the allocator "this block is available again", so a later allocation may reuse those very bytes. Forget to call it and the block stays reserved for the life of the program even after you have lost all means of reaching it; that is a memory leak, the subject of guide 3, and in a long-running program leaks accumulate until you run out of memory.
free() comes with a small contract you must honour exactly. You may free a block once and only once: calling free() twice on the same pointer corrupts the allocator's bookkeeping and is undefined behaviour, the topic of guide 4. You may pass `free(NULL)` freely — it is defined to do nothing, which is handy in cleanup paths. But after `free(p)`, the pointer `p` still holds the old address even though the block is gone; touching it now is a use-after-free, one of the most dangerous bugs in all of C precisely because it often seems to work until the freed bytes get reused under you.
A peek under the hood
Where do these blocks actually come from? The allocator does not bother the operating system for each tiny request. Instead the C library asks the kernel for a large slab of address space up front and then hands out pieces of it from a free list — a private ledger of which chunks are in use and which are available. A `malloc(24)` walks that ledger for a free chunk big enough, marks it used, and returns a pointer just past a few bytes of bookkeeping it keeps for itself; a `free()` flips the chunk back to available. This is why `free()` needs no size argument: the allocator already wrote the block's size into those hidden bytes right before the address it gave you.
That ledger explains a subtler cost. As you allocate and free blocks of mixed sizes over time, the free space gets chopped into a patchwork of small holes separated by live blocks — fragmentation. You can end up with, say, 10 KiB free in total yet be unable to satisfy a single 4 KiB request because no one hole is that big. Good allocators fight this by splitting and recombining adjacent free chunks, but fragmentation is a real, inherent cost of manual allocation rather than a bug you can simply code away. The final guide in this rung opens the allocator all the way up; for now it is enough to know the heap is a managed pool, not infinite tape.
You now hold the four verbs of manual memory — malloc(), calloc(), realloc(), free() — and the contracts each one demands. Crucially, you have met those contracts honestly, with their failure modes in plain view: the unchecked `NULL`, the leak, the use-after-free, the double-free, the slow creep of fragmentation. These are not rare exotica; they are the everyday hazards of writing C, and the rest of this rung exists to make each one familiar enough that you handle it by reflex. Next we look harder at the question that quietly underlies all of them — for any given block, who owns it, and whose job is it to call free()?