int marks[100];
// 100 — decided at compile time, foreverDynamic Memory in C: Using malloc(), calloc(), realloc(), and free()
What is Dynamic Memory in C? Core Concept & Direct Summary
How do `malloc()`, `calloc()`, `realloc()`, and `free()` manage heap memory allocation, resizing, and deallocation?
Every container we've built in this series had the same limitation: you had to decide its size before the program ran.
What if the user has 250 marks? You overflow. What if they have 3? You've allocated 97 slots of wasted memory. And what if the size depends on data you only read at runtime — the number of lines in a file, records in a response, nodes in a growing list? A fixed declaration simply can't express it.
Dynamic memory allocation is C's answer: the ability to request memory while the program is running, sized by values the compiler never saw. It's how you build containers that adapt — and it's the foundation of every dynamic data structure in existence: linked lists, trees, resizable buffers, hash tables.
Four functions do the whole job: malloc, calloc, realloc, and free. The syntax is easy. The discipline — who owns what, when it's freed, what happens when allocation fails — is where the real learning lives, and it's what this page focuses on.
(Suggested image: the hero graphic — stack and heap regions of memory, with a stack variable shrinking away at a function return while a heap block persists across functions, marked "you control this lifetime")
How Does Dynamic Memory in C Work? In-Depth Mechanics & Architecture
The Stack vs the Heap
Two different memory regions, with very different rules:
The key difference is lifetime. A stack variable dies when its block exits — no matter what pointers still reference it. That's the dangling-pointer trap from the previous tutorial. A heap block lives until you explicitly release it, which means:
- Good: you can return a heap pointer from a function, store it in a struct, keep it for the whole program run
- Bad: if you forget to free it, it stays allocated forever — a memory leak
That trade — full lifetime control in exchange for full lifetime responsibility — is the whole deal with dynamic memory.
(Suggested image: a timeline comparing the two — stack frame appearing and disappearing with a function call, heap block persisting across many calls until an explicit free())*
malloc(): Allocate Uninitialized Memory
The primary allocation function. "Memory allocation."
The canonical usage:
Sample Run:
Four things to notice:
malloctakes bytes, not element counts. So you always multiply:n * sizeof(int). Forgetting thesizeofgives younbytes — enough forn/4ints, and a heap overflow waiting to happen.- The return is
void *. It converts to any pointer type automatically in C, soint *arr = malloc(...)needs no cast. (Adding a cast is a C++ habit; it's unnecessary in C and can hide a missing#include <stdlib.h>.) NULLcheck is mandatory. Allocation can fail when memory runs out — and it will, eventually, under load. A missing check means you use a "pointer" that points at address 0, and crash.- The result is uninitialized.
mallocgives you raw memory containing whatever was there before — garbage. Same as an uninitialized local array. If you need zeros, usecalloc.
(Suggested image: the malloc call annotated — malloc(n * sizeof(int)) with the byte count called out, and the NULL-check branch shown as a required path)
calloc(): Allocate Zeroed Memory
calloc allocates and initializes every byte to zero. "Contiguous allocation." The signature takes a count and a size separately:
Output:
malloc vs calloc
Practical guidance: use calloc when you want a clean slate — accumulators, count arrays, matrices that start empty. Use malloc when you're about to write every element anyway, and zeroing would be wasted work.
That overflow note is more than trivia: malloc(n * sizeof(int)) computes the multiplication in your code, so a huge n can wrap around to a small number — and malloc happily returns a small block, which you then overflow. calloc receives the two values separately and most implementations detect the overflow and return NULL. For values from untrusted input, that's a meaningful safety difference.
(Suggested image: side-by-side calling cards for malloc and calloc, with the memory contents shown — garbage bytes vs all zeros)
realloc(): Resize an Existing Block
Need more (or less) space than you originally requested? realloc resizes:
What realloc actually does — and this is the part that matters:
- It may expand in place (if the allocator has room right after your block) — in which case your address is unchanged.
- Or it may allocate a new block, copy your data over, and free the old one — in which case your address changes.
You cannot know which happened, so you must always use the returned pointer. And there's a serious consequence: any other pointer into the original block becomes dangling if the block moved.
(That's the realloc case from the dangling pointers tutorial — worth re-reading if it didn't stick.)
The two critical realloc idioms:
Idiom 1: Never assign directly back to the original pointer.
The difference is a memory leak versus correct behavior. realloc returns NULL on failure without freeing the original block — so assigning NULL over your only pointer to it strands the memory permanently.
Idiom 2: Use a growth pattern.
Growing one element at a time is O(n²) work overall, because each realloc may copy the entire block. Real code grows geometrically:
Doubling gives amortized O(1) appends — this is exactly how C++'s std::vector and dynamic arrays in every language work internally. It's a genuinely important pattern to know.
(Suggested image: the realloc diagram — two cases, in-place expansion (same address) vs move-and-copy (new address, old block freed), with the dangling-pointer warning on the move case)
free(): Release the Memory
One function, one argument, one job:
The rules of free:
- Free only what you allocated.
freeon a stack variable, a string literal, or an interior pointer (like&arr[5]) is undefined behavior. You must pass the exact pointermalloc/calloc/reallocreturned. - Free exactly once. A second
freeof the same pointer is a double free — it corrupts allocator bookkeeping and can be exploited. free(NULL)is safe. Guaranteed by the standard to do nothing. That's why nulling after free makes cleanup re-runnable.freedoesn't change your pointer. It releases the memory; the pointer still holds the old address. That's a dangling pointer until you set it toNULL.- Free doesn't shrink anything you can observe. The memory returns to the allocator for reuse — that's it.
The ownership rule, stated plainly:whoever allocates must free, or must hand ownership to someone who will — and the transfer must be documented. Every memory bug in C is ultimately a violation of this rule.
(Suggested image: the allocation lifecycle diagram — malloc → use → free → NULL, with the leak path (no free) and the dangling path (use after free) branching off as warning routes)
Putting It Together: A Dynamic Array That Grows
Here's the pattern above, complete and working — a resizable integer list:
Sample Run:
Watch the capacity column: 2 → 4 → 8 → 16. It doubles exactly when it fills, which is the geometric growth pattern that keeps appends cheap no matter how many you do.
This program demonstrates professional memory discipline throughout:
- Separate
countandcapacity— how many are used versus how many fit. This distinction is essential and appears in every real dynamic container. - Cleanup on partial failure —
listCreatefreeslistif the innermallocfails. Otherwise you'd leak the struct. - The safe realloc pattern — temp pointer, NULL check, then commit. If it fails, the existing data is untouched.
- Reverse-order cleanup —
listFreefreesdatabeforelist, becauselistcontains the pointer todata. Free the container last. - NULL tolerance —
listFree(NULL)andlistPrint(NULL)handle null gracefully, so callers can clean up without extra checks. conston the read-only function —listPrinttakesconst IntList *and modifies nothing.
This is genuinely the skeleton of std::vector, ArrayList, and every dynamic array in every language. You've now built one from scratch.
Memory Leaks: The Other Failure Mode
A leak is memory you allocated and never freed. The program keeps running, but the memory is gone — unreachable and unusable.
Why leaks are subtle: a single small leak is invisible. But call that function in a loop a million times and you've leaked 4 GB — on a small leak repeated often, which is exactly the shape real leaks take. In long-running programs (servers, daemons, embedded systems), leaks eventually exhaust memory and crash the process.
Common leak causes:
- Missing
freeon a normal path — the simplest case - Missing
freeon an early return path — a function returns early on an error and skips the cleanup - Losing the pointer —
arr = realloc(arr, ...)failing, or overwritingarrwith a newmalloc - Overwriting a pointer —
p = malloc(...)whenpalready held allocated memory - Not freeing nested allocations — a struct with allocated members; freeing the struct alone leaks its contents
- Ownership confusion — two functions both think the other will free it, so nobody does
The discipline that prevents leaks: for every malloc/calloc/realloc in your code, know exactly where the corresponding free happens. Write it in the comment. If you can't point at the line, you have a leak.
(Suggested image: a leak-accumulation graph — memory usage climbing steadily over iterations because each call allocates without freeing)
Detecting Memory Errors
Free tooling here, and it's worth using from day one.
AddressSanitizer — a compiler flag, works everywhere including Windows via Clang:
Catches leaks, use-after-free, double free, and heap buffer overflows — all with exact line numbers.
Valgrind (Linux/macOS):
Reports leaked blocks with the allocation stack trace:
Compiler warnings:
And the habit worth adopting: make a debug build target with sanitizers enabled and use it every time you run tests. Memory bugs that appear as mystery crashes in production show up as precise reports in a sanitized build.
Common Dynamic Memory Mistakes
- Forgetting the
sizeof—malloc(n)instead ofmalloc(n * sizeof(int)). You getnbytes, notnints. Heap overflow. - Not checking for
NULL— every allocation can fail. Check every one. - Sizing by the pointer type instead of the pointee —
malloc(10 * sizeof(char *))when you meant 10 characters. Wrong either way round is a classic. arr = realloc(arr, ...)— a failure leaks the original. Use a temporary.- Forgetting
freeentirely — the leak. - Freeing the same pointer twice — double free, allocator corruption. Null after free.
- Using memory after
free— dangling pointer, undefined behavior. - Freeing an interior pointer —
free(&arr[5])is invalid; free only the exact allocated pointer. - Freeing non-heap memory —
freeon a local variable or a literal. - Not initializing before reading —
mallocreturns garbage; usecallocfor a clean slate. - Losing the pointer on early return — the cleanup-on-error paths are where leaks hide. Consider a single cleanup label at the end of the function (the goto cleanup pattern from the goto tutorial).
- Forgetting nested frees — free the struct's allocated members before the struct.
- Integer overflow in the size calculation —
n * sizeof(int)can wrap for hugen, returning a small block you then overflow. Validatenagainst a sane maximum, or usecalloc(which checks).
(Suggested image: "13 Dynamic Memory Mistakes" checklist graphic — grouping leaks, dangling, and overflow categories)
Final Thoughts
Dynamic memory allocation is where C hands you real control over memory — and real responsibility with it. Four functions, one idea: lifetimes you manage yourself. Stack memory is automatic and safe; heap memory is flexible and entirely on you.
The rules reduce to a short list worth keeping close:
malloc(n * sizeof(type))for raw memory;calloc(count, size)for zeroed memory- Always check for
NULL— allocation can fail - Never assign
reallocdirectly to the original pointer — temp, check, commit - Free exactly once, then set to
NULL - Free in reverse order of nesting — members before containers
- Use a sanitizer in your debug builds — you will not find these bugs by reading code
Get those habits in place and you'll have crossed the hardest threshold in C. Everything after this — linked lists, trees, hash tables, file buffers, string builders — is built on exactly the mechanism you've just learned. This is genuinely the point where you stop writing C that handles data and start writing C that manages it.
Runnable C Code Example & Practical Implementation
#include <stdio.h>
#include <stdlib.h>
int main() {
// Allocate array of 5 integers
int *arr =(int*)calloc(5, sizeof(int));
if (!arr) return 1;
for (int i = 0; i < 5; i++) arr[i] =(i + 1) * 10;
// Resize array to 8 integers
int *temp =(int*)realloc(arr, 8 * sizeof(int));
if (temp != NULL) {
arr = temp;
arr[5] = 60;
arr[6] = 70;
arr[7] = 80;
}
printf("Resized array elements:\
");
for (int i = 0; i < 8; i++) printf("%d ", arr[i]);
printf("\
");
free(arr);
arr = NULL;
return 0;
}Key Takeaways, Best Practices & Memory Safety Rules
malloc()/calloc() must have exactly one corresponding free(); use tools like Valgrind or AddressSanitizer (-fsanitize=address) to detect memory leaks automatically.Frequently Asked Questions & Technical Interview Insights
Technical Interview & Viva Questions
Frequently asked C programming interview questionsQ: Why should you store the return of `realloc()` in a temporary pointer first (`temp = realloc(ptr, new_size)`)?
Q: What is the difference between Heap and Stack memory in C?
Related Tools, Tutorials & Authoritative References
Recommended Interactive Tools
- Online C Compiler & Runner — Interactive online GCC sandbox to execute C code in real time.
- Online C Code Editor & IDE — Multi-file C programming environment with code formatting.
- C++ Programming Tutorial — Transition from procedural C to Object-Oriented C++ with STL.
- JavaScript Programming Tutorial — Master modern JavaScript fundamentals, DOM APIs, and async patterns.
Authoritative Standards & Citations
- ISO/IEC 9899 (C Language Standard) — Official international C specification by ISO/IEC JTC 1/SC 22/WG 14.
- GNU GCC Documentation — Official reference manual for the GNU Compiler Collection and C libraries.
- The C Programming Language (K&R) — Definitive classic work by Brian Kernighan & Dennis Ritchie at Bell Labs.
- C Reference Documentation (cppreference.com) — Verified reference covering C89 through C23 language features.