Notifications

No notifications

/Phase 3

Dynamic Memory (malloc / free)

Memory You Ask For at Runtime 🧠

So far our arrays have had fixed sizes known at compile time. But what if you don't know the size until the program runs (e.g., user enters n)?

Use dynamic memory from the heap — a large pool you can request bytes from with malloc and return with free.

#include <stdlib.h>

int n; scanf("%d", &n);

int *arr = malloc(n * sizeof(int)); // ask for n ints if (arr == NULL) { /* out of memory */ }

for (int i = 0; i < n; i++) arr[i] = i * i;

free(arr); // give it back when done arr = NULL; // good habit — avoid dangling pointer

The Four Heap Functions

FunctionPurpose
malloc(size)Allocate size bytes; contents are garbage
calloc(n, size)Allocate n × size bytes; zero-filled
realloc(p, new_size)Resize an existing block
free(p)Release a previously allocated block

The Pattern You'll Write 1000 Times

T *p = malloc(N * sizeof(T));
if (!p) { /* handle failure */ }

/* ... use p ... */

free(p); p = NULL;

Stack vs Heap (1-Minute Tour)

StackHeap
SpeedVery fastSlower
SizeSmall (~1 MB default)Huge (GBs)
LifetimeUntil function returnsUntil you free
Allocated byCompiler (auto)You (malloc)
Used forLocals, parametersBig or long-lived data

On this page

Detailed Theory

Dynamic memory is the difference between a toy program and a real one. As soon as your data depends on user input, file size, or the network, you'll need the heap.

Why Not Just Use Big Arrays?

int arr[1000000];   // ❌ — 4 MB on the STACK, will overflow

The stack is small (often 1 MB). For anything large or whose size isn't known at compile time, use the heap:

int *arr = malloc(1000000 * sizeof(int));   // ✅ on the heap

malloc — The Workhorse

void *malloc(size_t size);

  • Asks the OS for size bytes.
  • Returns a void * — a pointer to "anything". You assign it to a typed pointer.
  • Returns NULL if allocation fails (out of memory).
  • The bytes are uninitialised — contents are garbage.
int *p = malloc(10 * sizeof(int));   // 10 ints = 40 bytes
if (p == NULL) {
    fprintf(stderr, "Out of memory\n");
    return 1;
}

> Use sizeof(*p) instead of sizeof(int) — if you change the type later, you only edit the declaration: > >

> int *p = malloc(n * sizeof(*p));   // future-proof
>

calloc — malloc + Zero-Fill

int *p = calloc(10, sizeof(int));   // 10 ints, all zeroed

Use calloc when you need a clean slate (avoids garbage). Slightly slower than malloc because of the zeroing.

realloc — Resize an Allocation

int *p = malloc(10 * sizeof(int));
/* ... need more space ... */
int *new_p = realloc(p, 20 * sizeof(int));
if (new_p == NULL) {
    /* failure — old p is still valid; don't lose it */
    free(p);
    return 1;
}
p = new_p;     /* only overwrite p AFTER success */

realloc may move the block to a new address (if it can't grow in place), so always reassign through a temporary.

free — Always Pair With malloc

Every malloc/calloc/realloc must be matched by exactly one free. Forgetting causes a memory leak:

for (int i = 0; i < 1000; i++) {
    int *p = malloc(1024);
    /* forgot free(p);  →  1 MB leaked per iteration */
}

A long-running program with leaks will eventually crash.

The Three Memory Bugs You Will Write

1. Memory Leak

Allocated, never freed.

char *s = malloc(100);
return;     // ❌ leaked

2. Double Free

free called twice on the same block.

free(p);
free(p);    // ❌ undefined — heap corruption

Habit: free(p); p = NULL;free(NULL) is safe and a no-op.

3. Use After Free

Reading or writing memory after free.

free(p);
p[0] = 5;   // ❌ undefined — that memory is now whoever's

Same fix: set to NULL after free, and check before use.

Off-By-One With sizeof

int *p = malloc(10);            /* 10 BYTES, not 10 ints! Probably a bug. */
int *p = malloc(10 * sizeof(int));   /* ✅ 10 ints */
char *s = malloc(strlen(name));      /* ❌ no room for '\0' */
char *s = malloc(strlen(name) + 1);  /* ✅ room for terminator */

Always think in bytes when calling malloc.

Tools That Catch These Bugs

ToolCatches
-fsanitize=address (Clang/GCC)use-after-free, leaks, overflows
valgrindleaks, uninit reads, invalid frees
Static analyzers (clang --analyze)many issues at compile time

When learning, always compile with -Wall -Wextra -fsanitize=address -g. The sanitizer prints exactly where the bug is.

Dynamic Array Pattern

int *arr = NULL;
size_t cap = 0, len = 0;

void push(int v) { if (len == cap) { cap = cap ? cap * 2 : 4; int *tmp = realloc(arr, cap * sizeof(*arr)); if (!tmp) { /* handle */ return; } arr = tmp; } arr[len++] = v; }

This is the heart of every "growable list" in every language. Java's ArrayList, Python's list, JS's Array — all built on this idea.

2-D Arrays on the Heap

int rows = 3, cols = 4;
int **grid = malloc(rows * sizeof(int *));
for (int r = 0; r < rows; r++) {
    grid[r] = malloc(cols * sizeof(int));
}

grid[1][2] = 42;

/* Free in reverse */ for (int r = 0; r < rows; r++) free(grid[r]); free(grid);

Memory Cheat-Sheet

NeedUse
n bytes, garbagemalloc(n)
n × T, zeroedcalloc(n, sizeof(T))
Resizerealloc(p, new_size)
Releasefree(p); p = NULL;
Always checkif (p == NULL) { ... }

Master malloc/free and you can build any data structure C is famous for: linked lists, trees, hash tables, graphs.