Dynamic Memory Management
Lesson 5 of 8
5Dynamic Memory Management
In C, you're personally responsible for requesting memory and giving it back; there's no Garbage Collector doing that for you like in Python or JavaScript.
What the picture shows: malloc reserves a block, you use it, then free returns it. After free the pointer dangles, and writing through it is a bug.
Map of this part
The concepts in order, click any one to jump to it:
Two ways into the same idea
In Python a cleaner comes and collects papers you no longer need. In C there is no cleaner: you book a room yourself when you need it and hand it back yourself when done. Forget to hand it back and the building slowly fills up; hand it back and walk in again and you may find someone else there.
malloc and calloc reserve memory on the heap and return a pointer (or NULL on failure), realloc resizes, and free releases it. The classic bugs: a memory leak, use-after-free, double free, and writing out of bounds.
The Core Idea: Stack vs. Heap
Figure 1, stack variables are limited in size and managed automatically; heap memory is much larger but entirely your responsibility, from allocation to release.
1malloc
Requests a memory block of a given byte size from the heap, and returns a pointer to its start (or NULL if the request fails). Behind the scenes, the allocator reserves a small extra bit of space right before the pointer you actually receive, storing the block's own size there, this is exactly how free later knows how many bytes to return to the system without you telling it again.
2free
Returns that block to the system for reuse. Every successful malloc must be matched by exactly one free. In practice, free doesn't "erase" or zero out the memory, it just adds that block to an internal list of "available blocks" the allocator keeps, so it can hand it back out on a future malloc call instead of requesting fresh memory from the operating system. This means the old values can genuinely still sit in those bytes for a short while after freeing, which is exactly why using memory right after freeing it can "seem to work" for a moment, until it gets reallocated to something else and its contents suddenly change underneath you.
3Memory Leak
Forgetting to call free on a block that's no longer used, the memory stays reserved and "lost" until the program ends entirely. The real danger shows up in a long-running program, or one that allocates millions of times (like a server that never stops running): every small leak piles on top of the last, until the process has genuinely exhausted the memory available to it, and the system either starts refusing new malloc requests or slows to a crawl, this is usually the cause behind programs that "run fine for days, then suddenly crash" in production.
4Dangling Pointer
Using a pointer after the memory it pointed to has been freed with free, dangerous undefined behavior that might seem to "work" sometimes then crash suddenly. The sharpest danger shows up when free is called twice on the same pointer (a Double Free): because the allocator keeps internal bookkeeping (like a block's size and its place in the list of free blocks) stored directly inside that same memory, freeing it twice corrupts that internal bookkeeping, which can cause an immediate crash or, in the worst case, a genuine security vulnerability that can be exploited to run unwanted code, which is why a Double Free is classed among the most dangerous memory bugs in C.
How It Works, Step by Step: An Array Sized at Runtime
int *create_array(int size)
{
int *arr;
int i;
arr = malloc(sizeof(int) * size); /* request size integers */
if (arr == NULL)
return (NULL); /* always check malloc for failure */
for (i = 0; i < size; i++)
arr[i] = i * i;
return (arr);
}
int main(void)
{
int *nums = create_array(5);
if (nums != NULL)
{
/* use nums[0..4] here: 0, 1, 4, 9, 16 */
free(nums); /* free it as soon as you're done with it */
nums = NULL; /* good habit: never leave a dangling pointer */
}
return (0);
}
malloc(size) instead of malloc(sizeof(int) * size) when allocating an integer array. This reserves only size bytes instead of size full elements (each int is usually 4 bytes), writing past the actually allocated memory.
Worked Example: Variadic Functions and Function Pointers
#include <stdarg.h>
/* sums an unknown-in-advance number of integers */
int sum_all(int count, ...)
{
va_list args;
int total = 0, i;
va_start(args, count); /* start reading after the last known argument (count) */
for (i = 0; i < count; i++)
total += va_arg(args, int); /* read the next argument as int each time */
va_end(args); /* finish reading and clean up */
return (total);
}
/* function pointer: allows passing a whole behavior as an argument to another function */
int compare_asc(int a, int b) { return (a - b); }
int compare_desc(int a, int b) { return (b - a); }
void apply_compare(int a, int b, int (*cmp)(int, int))
{
printf("%d\n", cmp(a, b)); /* cmp could be compare_asc or compare_desc */
}
Figure 2, Both concepts build flexibility on top of pointers: variadic functions allow a non-fixed number of arguments, and function pointers allow passing "any behavior" as an argument instead of a plain value.
Deep Dive: Allocation, Ownership and Function Pointers
Dynamic memory gives you power and responsibility together. Here we detail why we need it at all, how each allocation function behaves, what memory ownership means, and then move to function pointers and variadic functions as tools that make code flexible.
1. Automatic versus dynamic allocation
| Criterion | Automatic (stack) | Dynamic (heap) |
|---|---|---|
| Who allocates and frees | The compiler automatically | You, with malloc and free |
| Lifetime | Ends when the function exits | Until you free it |
| Size | Usually must be known at compile time | Decided at run time |
| Capacity | Limited (about 8 MB) | Bounded by available memory |
| Speed | Very fast | Slower |
We need the heap in two situations: when we only know the size at run time (the count of what the user enters), and when we want a value to outlive the function that created it. A common mistake is for a function to return a pointer to a local variable: it dies when the function exits and the pointer becomes dangling.
2. The family of allocation functions
| Function | What it does | What to remember |
|---|---|---|
malloc(n) | Reserves n bytes that are uninitialized (random values) | Returns NULL on failure: always check |
calloc(k, s) | Reserves k × s bytes and zeroes them | Checks for overflow in the multiplication |
realloc(p, n) | Resizes an existing block, preserving its content | It may move the block to a new address, so rely on the returned value |
free(p) | Returns the block to the system | Does nothing for NULL, and is dangerous when called twice |
The subtlest thing about realloc is that it can fail, returning NULL and leaving the original block intact. If you write p = realloc(p, n) directly and it fails, you lose the original pointer and leak memory. The fix: put the result in a temporary pointer first and check it before replacing.
Figure 3, The safe resize pattern: do not lose the old pointer before the new one is confirmed.
3. Ownership and freeing
At every moment each block you reserved must have one owner responsible for freeing it. If you copy the pointer to another pointer they are two aliases of the same block, so do not free it twice. An important mistake is forgetting to free on early exit paths, because every return in the middle of a function is a possible leak. On partial failure (you reserved three blocks and the fourth failed) you must free the previous three before leaving. The function exit(n) ends the whole program at once with an exit code, giving you a way to stop at an unrecoverable error, but it does not perform on your behalf the cleanup logic you need.
4. Function pointers
A function also lives in memory: its instructions sit in the text region, and it has a start address. A function pointer is a variable holding that address, which lets you pass a whole behavior as an argument. Its form: int (*op)(int, int) means "op is a pointer to a function taking two integers and returning an integer". Its value is that you write generic code that does not know the details: such as a function applying an operation to each element of an array and receiving the operation itself as an argument, or a table mapping a command name to its function. The C library uses it in qsort, which sorts any type because the comparison is passed to it.
Figure 4, A function pointer holds the start address of a function in the text region, and can be switched at run time.
5. Variadic functions and const
A variadic function receives an unspecified number of arguments after at least one fixed argument. It relies on three elements of stdarg.h: va_start begins reading, va_arg pulls the next argument as the type you specify, and va_end cleans up. The function knows neither the number nor the types of its arguments, so you must tell it by an agreed means: an explicit count in a fixed parameter, a format string, or a sentinel value ending the list. Any mismatch between what you promised and what you passed is undefined behavior.
const is a promise to the compiler that you will not modify the value through this name. const char *s means the text s points to is read-only, while char *const s means the pointer itself does not change but the text is modifiable. The rule: read the declaration from right to left. Using it in function parameters shows the code reader which parameter the function only reads and which it may change.
Try it yourself
What happens if you never call free on memory you no longer need?
Every successful malloc needs exactly one free. The leak accumulates in programs that run for a long time.
What is a dangling pointer?
Undefined behaviour: it may “work” sometimes and then crash. The rule: set the pointer to NULL after free.
When does malloc return NULL?
Checking the result before use is mandatory: dereferencing NULL crashes.
Quick Review Flashcards
Check Your Understanding
Why do we set nums = NULL after free(nums) in the example above, even though the memory is already freed?
To prevent accidentally using nums later as a dangling pointer to freed memory, reading or writing through a NULL pointer causes an immediate, obvious crash instead of silent, dangerous undefined behavior.
What happens if you call free on the same pointer twice?
Undefined behavior called a Double Free, it can corrupt the program's internal memory-management structures and cause a later crash that's hard to trace back to its real cause.
A function returns the address of a local array; what is the problem?
The array dies when the function exits, so the pointer is dangling.
You reserved three blocks and the fourth failed; what do you do before leaving?
Free the three that succeeded, then handle the failure.
Resources for this part
Sorted by kind: start with the official docs when you want the precise reference, or with an article or video when you want the explanation first.
Resources for this lesson
Grouped by type. Start with the official docs for precision, or an article or video for the explanation.
Official docs2
Done with this lesson?
Sign in to save your progress.