Dynamic Memory & Lifetimes

Every C object lives somewhere. Stack objects are created and destroyed automatically as functions return; heap objects are created by your code with malloc and destroyed by your code with free. The heap is where flexibility lives — and where leaks, dangling pointers, and double-frees are born. This page makes ownership a conscious act.

The Process Memory Layout

When your program runs, the operating system gives it one address space with distinct regions. Knowing them explains most C behavior — including why local variables vanish and why large arrays belong on the heap.

Process memory layout: text, data, BSS, heap growing up, stack growing down

Figure 1 — the classic C memory map: text (code), data/BSS (globals), heap (malloc grows up), and the stack (grows down, one frame per call).

int global_init = 5;        // lives in .data, alive for the whole run
static int global_zero;     // lives in .bss (zero-filled), alive for the run

int main(void) {
    int local = 7;          // lives on the STACK, dies when main returns
    int *p = malloc(64);    // lives on the HEAP until you free(p)
    free(p);                // heap object destroyed here
    return 0;
}

The stack grows downward and is strictly LIFO — that is exactly why you may not return a pointer to a local. The heap grows upward, outlives the function that created it, and must be managed by hand.

malloc, calloc, realloc, free

Four functions cover heap management. malloc reserves bytes (uninitialized); calloc reserves and zeroes (safer default); realloc resizes in place if possible, possibly moving the block; free releases exactly one malloc-family block. Every allocation returns NULL on failure — check it, and check the multiplication: malloc(n * sizeof(T)) can overflow for huge n.

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int n = 5;
    int *arr = calloc(n, sizeof(int));   // zero-initialized int vector
    if (arr == NULL) {                   // allocation failed: handle it
        fprintf(stderr, "out of memory\n");
        return 1;
    }
    for (int i = 0; i < n; i++) {
        arr[i] = (i + 1) * 10;           // fill as if it were any array
    }
    n = 8;                               // grow it: realloc copies old bytes
    int *grown = realloc(arr, (size_t)n * sizeof(int));
    if (grown == NULL) {                 // realloc failed: arr is STILL valid
        free(arr);                       // release the original block, bail
        return 1;
    }
    arr = grown;                         // take ownership of the new block
    printf("arr[7] = %d\n", arr[7]);     // garbage — we only zeroed 5 slots
    free(arr);                           // one free per successful realloc
    return 0;
}

Note the realloc idiom: assign to a temporary first, because on failure realloc leaves the original block alive — overwriting your pointer would leak it.

The Three Classic Memory Bugs

Real-world C bugs reduce to three patterns. Learn to recognize them by smell — that is cheaper than by crash:

  • Leak — you allocate but never free. The program's memory grows until the OS kills it. Fix: for every malloc there must be a matching free on every path, including error paths.
  • Double free — you call free twice on one block. Fix: set the pointer to NULL after freeing; freeing NULL is legal and harmless.
  • Use-after-free — you touch a block after freeing it. The bytes may still "work" for a while, which makes this the most dangerous bug: it is exploitable. Fix: never keep aliases to a freed block.
#include <stdlib.h>

int main(void) {
    // LEAK: temporary never freed
    {
        int *tmp = malloc(100);
        // ... no free(tmp) on this path
    }

    // DOUBLE FREE + USE-AFTER-FREE: the same pointer twice
    int *p = malloc(10);
    free(p);
    p = NULL;           // idiom: NULL the pointer right after freeing
    free(p);            // legal now — free(NULL) is a no-op
    return 0;
}

The compiler cannot see any of these. The AddressSanitizer can — this is exactly the case for -fsanitize=address from the setup page. Add it during development and the three bugs above stop being mysteries.

Ownership — Who Frees?

Every heap block must have exactly one owner: the code that ultimately calls free. Decide the owner when you allocate, and write the free next to the malloc in your head. Two idioms keep ownership visible:

#include <stdlib.h>
#include <string.h>

// IDIOM 1: the caller transfers ownership of its input
char *shout(char *text) {          // caller's buffer, caller frees
    char *up = malloc(strlen(text) + 1);
    if (up == NULL) return NULL;
    for (size_t i = 0; text[i]; i++)
        up[i] = (text[i] >= 'a' && text[i] <= 'z') ? text[i] - 'a' + 'A' : text[i];
    up[strlen(text)] = '\0';
    return up;                     // caller owns and must free(up)
}

Document the rule in the function comment: "returns a heap allocation; caller frees". Ambiguity about ownership is how leaks and double-frees sneak into a codebase.

Stack vs Heap — When to Choose

ConcernStackHeap
AllocationAutomatic on entryExplicit malloc
SpeedOne pointer moveSystem call + bookkeeping
Size limit~1–8 MB, fixed per threadUp to available RAM
LifetimeUntil the block endsUntil free
Error riskOverflow (deep recursion)Leaks, dangling, double-free

Rule of thumb: small and short-lived objects go on the stack; large, long-lived, or size-unknown objects go on the heap. The collections page builds exactly this kind of heap-managed structure.

Next: error handling — the C way of admitting failure.