The Heap and Dynamic Allocation
malloc, calloc, realloc, free — how to ask the runtime for memory and give it back
The stack is fast but rigid: every allocation dies when its function
returns. To make memory outlive the function that created it —
to build a linked list, a dynamic array, a parsed AST, anything —
you ask the runtime for a piece of the heap with malloc and
return it later with free.
Manual heap management is C's most distinctive (and most dangerous) feature. This page is the long, careful introduction.
The four heap functions
| Function | Purpose | Returns |
|---|---|---|
malloc(n) | Reserve n bytes, contents uninitialized | void * or NULL on failure |
calloc(count, size) | Reserve count * size bytes, zeroed | void * or NULL |
realloc(p, n) | Resize an existing block; may move it | void * (new ptr) or NULL |
free(p) | Release a block previously returned by malloc/calloc/realloc | void |
They all live in <stdlib.h>.
Your first malloc
Three rules visible in that tiny example:
- Always check for
NULL.malloccan fail. - Initialize before reading.
mallocdoes not zero memory; reading uninitialized bytes is undefined behavior. - Free exactly once. Not zero times (leak). Not two times (double-free, often crash). Exactly once, then stop using the pointer.
A growable array of N ints
This is the most common use of malloc: you only know the size at
runtime.
The `sizeof *ptr` idiom
Writing malloc(n * sizeof *arr) instead of malloc(n * sizeof(int))
means the line stays correct if you ever change arr's element type.
It is a small habit that prevents large bugs.
calloc: zeroed and overflow-checked
calloc(count, size) does two extra things compared to
malloc(count * size):
- It zeroes the memory before returning.
- It checks
count * sizefor integer overflow.
Prefer calloc whenever you would otherwise call malloc followed
by memset(p, 0, ...).
realloc: grow or shrink
realloc(p, new_size) is the resizing primitive. It may:
- expand the existing block in place (best case), or
- allocate a new block, copy the old contents, and free the old one,
- or return
NULL(the old block is still valid in this case).
Never assign realloc's result back over the only pointer
p = realloc(p, n); leaks the original block if realloc fails. The
safe pattern is to use a temporary.
Building a dynamic array (mini std::vector)
A small, complete vec type to show how growable arrays really work.
Doubling the capacity each time gives amortized O(1) push. This is
exactly how std::vector<T> in C++ and Vec<T> in Rust grow.
Ownership: who frees what?
C has no automatic memory management. Someone must call free on
every heap block exactly once. The way to keep this manageable is to
make ownership explicit:
- The function that creates a block usually documents whether it returns the block to the caller (caller frees) or keeps it internally (the library frees later).
- The function that takes a block usually documents whether it takes ownership (and will free it) or just borrows it (caller still frees).
- A struct with a heap-allocated member usually has a matching
_freefunction (or_destroy, or_release) that knows how to clean up.
Allocating into a caller's pointer
Sometimes the function needs to allocate and replace the caller's pointer with the new address. That's a pointer-to-pointer parameter.
Memory leaks
A leak is a heap block that the program no longer has any pointer to but has not freed. The block sits there until the process exits. Long-running programs (servers, daemons, GUIs) that leak slowly may eventually exhaust memory and crash.
We will see leaks again — alongside double-frees and use-after-frees — in the Memory Bugs chapter.
Practice: average of N numbers from the heap
Allocate a heap array of N = 5 ints, fill it with the values 10, 20, 30, 40, 50 (in that order), print their integer average, then free the array. The output must be exactly 30.
Practice: heap-allocated string copy
Implement char *my_strdup(const char *s) that returns a heap-allocated copy of s (caller frees it). The provided main calls it, prints the copy, and frees it.
Test your understanding
What is the safest way to handle a failed realloc?
Assume it succeeded and keep using the same pointer.
Assign the result to a temporary first; if NULL, free the old block (which is still valid) and bail out.
Call realloc again immediately; the second call usually succeeds.
Use free on the original block and then malloc a new one.
Why does malloc not zero-initialize the memory it returns?
The standard requires that it does.
Zeroing is impossible without kernel support.
Zeroing has a cost; programs that are about to overwrite every byte anyway should not pay it. calloc exists for when you actually want zeros.
Because malloc always returns memory that was previously freed, which is already zeroed.
After free(p);, what should you do with p?
Read it once more to confirm the contents were cleared.
Pass it to free again to be safe.
Print it to stdout for debugging.
Stop using it (and ideally set it to NULL so accidental later uses become immediate NULL-pointer crashes instead of silent corruption).