Topic 78 of 100 Pointers in C Interactive Tutorial

Dynamic Memory in C: Using malloc(), calloc(), realloc(), and free()

15 mins read Last updated: 22 Aug 2026 36,800 views
C Compiler 0.00%

What is Dynamic Memory in C? Core Concept & Direct Summary

Core Objective & Problem Statement

How do `malloc()`, `calloc()`, `realloc()`, and `free()` manage heap memory allocation, resizing, and deallocation?

Direct Answer Summary

Every container we've built in this series had the same limitation: you had to decide its size before the program ran.

main.c
Run Online
int marks[100];
// 100 — decided at compile time, forever

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:

StackHeap
Where variables come fromLocal declarationsmalloc/calloc/realloc
SizeFixed, small (typically 1–8 MB)Large (limited by available RAM)
LifetimeAutomatic — ends with the blockUntil you free it
SpeedVery fastSlower (allocator bookkeeping)
Size known atCompile timeRuntime

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."

main.c
Run Online
#include <stdlib.h>
void *malloc(size_t size);
// size in BYTES, not elements

The canonical usage:

main.c
Run Online
#include <stdio.h>
#include <stdlib.h>
int main(void) {
    int n;
    printf("How many numbers? ");
    scanf("%d", &n);
    if (n <= 0) {
        puts("Invalid count.");
        return 1;
    }
    // Request n ints worth of bytes    int *arr = malloc(n * sizeof(int));
    // ALWAYS check for failure    if (arr == NULL) {        puts("Memory allocation failed!");        return 1;    }
    // Now use it exactly like an array    for (int i = 0; i < n; i++) {        arr[i] = (i + 1) * 10;    }
    printf("Created %d values: ", n);
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    putchar('\n');
    // Release the memory — the caller owns it    free(arr);    arr = NULL;
    return 0;
}

Sample Run:

main.c
Run Online
How many numbers? 5Created 5 values: 10 20 30 40 50

Four things to notice:

  1. malloc takes bytes, not element counts. So you always multiply: n * sizeof(int). Forgetting the sizeof gives you nbytes — enough for n/4 ints, and a heap overflow waiting to happen.
  2. The return is void *. It converts to any pointer type automatically in C, so int *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>.)
  3. NULL check 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.
  4. The result is uninitialized.malloc gives you raw memory containing whatever was there before — garbage. Same as an uninitialized local array. If you need zeros, use calloc.

(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:

main.c
Run Online
void *calloc(size_t count, size_t elementSize);
main.c
Run Online
#include <stdio.h>
#include <stdlib.h>
int main(void) {
    int n = 5;
    // No multiplication needed — calloc takes count and size separately    int *arr = calloc(n, sizeof(int));
    if (arr == NULL) {
        puts("Allocation failed!");
        return 1;
    }
    printf("calloc result: ");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
        // all zeros    }    putchar('\n');
        free(arr);
        return 0;
    }

Output:

malloc vs calloc

malloc(n * size)calloc(n, size)
InitializationGarbageAll zeros
ArgumentsOne (total bytes)Two (count, element size)
Overflow safetyn * size can overflow before the callSeparate arguments — implementations check internally
SpeedSlightly faster (no zeroing)Slightly slower (zeroes memory)
Best forBuffers you'll fully overwriteCounters, flags, arrays you'll fill partially

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:

main.c
Run Online
void *realloc(void *ptr, size_t newSize);
main.c
Run Online
int *arr = malloc(5 * sizeof(int));
// ... use 5 elements ...
int *temp = realloc(arr, 10 * sizeof(int));
// grow to 10if (temp == NULL) {    // realloc failed — arr is STILL VALID and unchanged    free(arr);    return 1;}arr = temp;                                     // success — commit

What realloc actually does — and this is the part that matters:

  1. It may expand in place (if the allocator has room right after your block) — in which case your address is unchanged.
  2. 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.

main.c
Run Online
int *arr = malloc(10 * sizeof(int));
int *mid = &arr[5];
// points into the block
arr = realloc(arr, 1000 * sizeof(int));
// may have MOVED
printf("%d\n", *mid);
// ✗ dangling if it 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.

main.c
Run Online
// ✗ WRONG — if realloc fails, you've lost the original pointer (memory leak!)arr = realloc(arr, newSize);
// ✓ CORRECT — use a temporary, check, then commitint *temp = realloc(arr, newSize);
if (temp != NULL) {
    arr = temp;
} else {
    // handle failure — arr is still valid and still allocated
}

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:

main.c
Run Online
size_t capacity = 4;
int *arr = malloc(capacity * sizeof(int));
size_t count = 0;
void add(int value) {
    if (count == capacity) {
        capacity *= 2;
        // double it        int *temp = realloc(arr, capacity * sizeof(int));
        if (temp == NULL) {
            // handle — but do NOT lose arr            return;
        }
        arr = temp;
    }
    arr[count++] = value;
}

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:

main.c
Run Online
int *arr = malloc(100 * sizeof(int));
// ... use it ...free(arr);
arr = NULL;
// the habit from the NULL pointers tutorial

The rules of free:

  • Free only what you allocated.free on a stack variable, a string literal, or an interior pointer (like &arr[5]) is undefined behavior. You must pass the exact pointer malloc/calloc/realloc returned.
  • Free exactly once. A second free of 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.
  • free doesn't change your pointer. It releases the memory; the pointer still holds the old address. That's a dangling pointer until you set it to NULL.
  • 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:

main.c
Run Online
#include <stdio.h>
#include <stdlib.h>
typedef struct {
    int *data;
    // heap block    size_t count;         // how many are used    size_t capacity;      // how many fit} IntList;
    // Create an empty listIntList *listCreate(size_t initialCapacity) {    IntList *list = malloc(sizeof(IntList));    if (list == NULL) return NULL;
    list->data = malloc(initialCapacity * sizeof(int));
    if (list->data == NULL) {
        free(list);
        // clean up the partial allocation!        return NULL;    }
        list->count = 0;
        list->capacity = initialCapacity;
        return list;
    }
    // Append a value, growing if necessary. Returns 1 on success, 0 on failure.int listAppend(IntList *list, int value) {    if (list == NULL) return 0;
    if (list->count == list->capacity) {
        size_t newCapacity = list->capacity * 2;
        int *temp = realloc(list->data, newCapacity * sizeof(int));
        if (temp == NULL) {
            return 0;
            // original data still intact        }
            list->data = temp;
            list->capacity = newCapacity;
        }
        list->data[list->count++] = value;
        return 1;
    }
    // Print the listvoid listPrint(const IntList *list) {    if (list == NULL) {        puts("(null list)");        return;    }
    printf("[ ");
    for (size_t i = 0; i < list->count; i++) {
        printf("%d ", list->data[i]);
    }
    printf("] (count=%zu, capacity=%zu)\n", list->count, list->capacity);
}
// Free everything the list ownsvoid listFree(IntList *list) {    if (list == NULL) return;
free(list->data);
// free the internal block first    free(list);                           // then the struct itself}
int main(void) {
    IntList *nums = listCreate(2);
    if (nums == NULL) {
        puts("Could not create list.");
        return 1;
    }
    puts("Appending values (capacity doubles when full):");
    for (int i = 1; i <= 10; i++) {
        if (!listAppend(nums, i * 5)) {
            puts("Append failed — out of memory.");
            break;
        }
        printf("After adding %3d: ", i * 5);
        listPrint(nums);
    }
    listFree(nums);
    return 0;
}

Sample Run:

main.c
Run Online
Appending values(capacity doubles when full):After adding 5: [ 5 ](count=1, capacity=2)After adding 10: [ 5 10 ](count=2, capacity=2)After adding 15: [ 5 10 15 ](count=3, capacity=4)After adding 20: [ 5 10 15 20 ](count=4, capacity=4)After adding 25: [ 5 10 15 20 25 ](count=5, capacity=8)After adding 30: [ 5 10 15 20 25 30 ](count=6, capacity=8)After adding 35: [ 5 10 15 20 25 30 35 ](count=7, capacity=8)After adding 40: [ 5 10 15 20 25 30 35 40 ](count=8, capacity=8)After adding 45: [ 5 10 15 20 25 30 35 40 45 ](count=9, capacity=16)After adding 50: [ 5 10 15 20 25 30 35 40 45 50 ](count=10, capacity=16)

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 count and capacity — how many are used versus how many fit. This distinction is essential and appears in every real dynamic container.
  • Cleanup on partial failure — listCreate frees list if the inner malloc fails. 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 — listFree frees data before list, because list contains the pointer to data. Free the container last.
  • NULL tolerance — listFree(NULL) and listPrint(NULL) handle null gracefully, so callers can clean up without extra checks.
  • const on the read-only function — listPrint takes const 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.

main.c
Run Online
void leaky(void) {
    int *data = malloc(1000 * sizeof(int));
    // ... use data ...    // forgot free(data)!
}
// ← data pointer dies, the 4000-byte block is permanently stranded

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:

  1. Missing free on a normal path — the simplest case
  2. Missing free on an early return path — a function returns early on an error and skips the cleanup
  3. Losing the pointer — arr = realloc(arr, ...) failing, or overwriting arr with a new malloc
  4. Overwriting a pointer — p = malloc(...) when p already held allocated memory
  5. Not freeing nested allocations — a struct with allocated members; freeing the struct alone leaks its contents
  6. 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:

main.c
Run Online
gcc -fsanitize=address -g -o program program.c./program

Catches leaks, use-after-free, double free, and heap buffer overflows — all with exact line numbers.

Valgrind (Linux/macOS):

main.c
Run Online
valgrind --leak-check=full ./program

Reports leaked blocks with the allocation stack trace:

main.c
Run Online
==1234== 4,000 bytes in 1 blocks are definitely lost in loss record 1 of 2
==1234== at 0x4C2FB0F: malloc(vg_replace_malloc.c:299)
==1234== by 0x4005A1: leaky(program.c:3)

Compiler warnings:

main.c
Run Online
gcc -Wall -Wextra -g program.c

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

  1. Forgetting the sizeof — malloc(n) instead of malloc(n * sizeof(int)). You get n bytes, not n ints. Heap overflow.
  2. Not checking for NULL — every allocation can fail. Check every one.
  3. 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.
  4. arr = realloc(arr, ...) — a failure leaks the original. Use a temporary.
  5. Forgetting free entirely — the leak.
  6. Freeing the same pointer twice — double free, allocator corruption. Null after free.
  7. Using memory after free — dangling pointer, undefined behavior.
  8. Freeing an interior pointer — free(&arr[5]) is invalid; free only the exact allocated pointer.
  9. Freeing non-heap memory — free on a local variable or a literal.
  10. Not initializing before reading — malloc returns garbage; use calloc for a clean slate.
  11. 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).
  12. Forgetting nested frees — free the struct's allocated members before the struct.
  13. Integer overflow in the size calculation — n * sizeof(int) can wrap for huge n, returning a small block you then overflow. Validate n against a sane maximum, or use calloc (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 realloc directly 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

main.c
Run Online
#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

Best Practice & Key Takeaways
Every 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 questions
Q

Q: Why should you store the return of `realloc()` in a temporary pointer first (`temp = realloc(ptr, new_size)`)?

A: If `realloc()` fails, it returns `NULL` without freeing the original memory block. If assigned directly `ptr = realloc(ptr, size);`, the original pointer is overwritten by `NULL`, leaking the original memory!
Q

Q: What is the difference between Heap and Stack memory in C?

A: Stack memory is fast, automatically managed, and small (fixed size per thread); Heap memory is manually managed, larger (up to system RAM), and persists until explicitly freed.

Related Tools, Tutorials & Authoritative References

Recommended Interactive Tools

Authoritative Standards & Citations