Dynamic Memory Management

Stack memory is automatic but rigid — sized at compile time and gone when a function returns. For data whose size you only know at runtime, or that must outlive the function that created it, C gives you the heap and four functions to manage it: `malloc`, `calloc`, `realloc`, and `free`. With that power comes C's heaviest responsibility: every allocation you make, you must free — exactly once, and never use again.

Post 3 introduced the stack; post 5 built arrays whose size was fixed at compile time. But real programs need memory whose size is decided at runtime (a buffer for a file of unknown length, an array that grows) and memory that survives past the function that created it. That’s the heap, and managing it manually is the defining responsibility — and the defining hazard — of C. This post covers how, and the bug classes you must avoid.

Stack vs. heap

C programs use two regions of memory, and choosing correctly is fundamental:

The rule of thumb: use the stack for small, fixed-size, function-local data (the default — it’s free and automatic), and the heap when you need runtime-determined size or data that outlives its creating function. The heap is where dynamic data structures (post 7) live, because their size grows and shrinks as the program runs.

The four functions

Heap memory is managed through <stdlib.h>:

#include <stdlib.h>

int *arr = malloc(n * sizeof(int));   // allocate space for n ints (uninitialized)
if (arr == NULL) { /* allocation failed — handle it */ }

int *zeroed = calloc(n, sizeof(int)); // allocate AND zero-initialize n ints

arr = realloc(arr, 2 * n * sizeof(int)); // resize the block (grow/shrink)

free(arr);      // return the memory to the heap when done
arr = NULL;     // defensive: avoid a dangling pointer

Two habits from the start: always check malloc/realloc for NULL (allocation can fail, especially for large requests), and sizeof the type in the size calculation so it stays correct across platforms.

Ownership: the mental model that saves you

C has no garbage collector, so someone must free every allocation — and C won’t tell you who. The discipline that makes this tractable is ownership: for every heap allocation, decide which piece of code is responsible for freeing it, and make that responsibility clear.

Ownership questions run through all C API design: When a function returns a malloc‘d pointer, does the caller now own it (and must free it)? When you store a pointer in a struct, who frees it — and when? Answer these deliberately, document them, and the manual memory management becomes manageable. Most memory bugs are really ownership confusion — two places both free it, or neither does. Think in terms of “who owns this and when does its life end,” and match every malloc with exactly one free on every path.

The four deadly bugs

Manual memory management creates four notorious bug classes. Recognizing them is half of avoiding them:

Notice how many defenses reduce to one habit: free(p); p = NULL; — it neutralizes use-after-free (crashes instead of corrupts) and double-free (free(NULL) is a no-op) in one move.

Tools: let the machine catch what you can’t

Because these bugs are silent and intermittent, C programmers rely on tools to find them — this is not optional for serious C. A memory checker like Valgrind (or the compiler’s AddressSanitizer, -fsanitize=address) runs your program and reports leaks, use-after-free, double-frees, and out-of-bounds accesses with the exact location:

valgrind --leak-check=full ./myprogram
gcc -fsanitize=address -g program.c -o program   # ASan alternative

Run your C programs under these routinely, especially before trusting them. They catch the memory errors that testing alone misses — the ones that “work on my machine” and crash in production. In C, the memory-checking tools are as much a part of the workflow as the compiler.

The bargain, made explicit

Dynamic memory is C’s control-and-responsibility bargain at its sharpest. You get precise, manual control over exactly when memory is allocated and freed — no GC pauses, no hidden overhead, memory usage you fully determine. In exchange, you own the entire lifecycle, and the four bug classes await any lapse. The way through is discipline made routine: think in ownership, match every malloc with one free on every path, adopt free(p); p = NULL; as a reflex, check every allocation for NULL, and run a memory checker. Do these consistently and manual memory management stops being terrifying and becomes just another engineering practice — the one that, more than any other, is what “knowing C” means. Next, we put the heap to work building data structures that grow at runtime.

Key takeaways

Further reading

Sources & References

malloc/calloc/realloc/free and their hazards