Embedded C

Dynamic Memory: malloc, calloc, realloc, free

Dynamic allocation is the default tool on a desktop and the tool many embedded/safety-critical codebases restrict or ban outright — not from tradition, but because a heap on a device with a few kilobytes of RAM and no OS to reclaim memory fails in ways a desktop application never has to think about. Understanding exactly what each function does, and exactly how a heap fails, is what makes "we don't use malloc here" a design decision instead of a superstition.

Quick reference
What it is
Runtime-managed memory requested from the heap with malloc/calloc/realloc and returned with free.
Mental model
The heap is a shared parking lot with no attendant — you have to remember which spot you parked in, never park somewhere you already left, and never assume a spot is still yours after you've given up the ticket.
Key numbers
4 classic bug categories (leak, double-free, use-after-free, overflow) account for the overwhelming majority of real-world heap bugs.
Reach for it when
Almost never after initialization on a hard-real-time or safety-critical embedded target — reach for a static array or a fixed-size memory pool instead, and reserve this page for understanding why.

malloc

malloc(size) requests size bytes of uninitialized memory from the heap, returning a void* to it — or NULL if the request can't be satisfied. On a device with a heap measured in kilobytes, failure is a realistic outcome that has to be handled, not a theoretical edge case a desktop program can shrug off.

uint8_t *buf = malloc(64);
if (buf == NULL) {
    handle_allocation_failure();   // this branch is not optional here
    return;
}

calloc

calloc(count, size) allocates count * size bytes and zero-initializes them — and, unlike doing the multiplication yourself, does it with an internal overflow check. malloc(count * size) can silently overflow the multiplication on a platform with a narrow size_t, handing back a successful allocation that's far smaller than intended; calloc is specified to detect that and fail instead.

struct sensor_reading *readings = calloc(num_sensors, sizeof(*readings));
// every field of every element starts at zero — no manual memset needed

realloc

realloc(ptr, new_size) resizes a previous allocation — possibly by moving it to a new address entirely, in which case it copies the old contents and the original pointer becomes invalid. The classic mistake overwrites the only reference to the original block with realloc's return value before checking it succeeded:

/* WRONG — leaks the original block on failure */
ptr = realloc(ptr, bigger_size);   // if this returns NULL, the old
                                    // block's address is gone forever

/* correct */
void *tmp = realloc(ptr, bigger_size);
if (tmp == NULL) {
    handle_allocation_failure();   // ptr is still valid here — free it if done
} else {
    ptr = tmp;
}

Heap fragmentation

A heap doesn't fail only when it's genuinely full — it fails when enough contiguous space doesn't exist, even though the total free memory would be plenty. This is fragmentation, and it's the specific reason a long-running device can start failing allocations after weeks of uptime despite never leaking a single byte.

Allocating A, B, and C, then freeing B, leaves a hole exactly B's size — a later allocation larger than that hole can't reuse it and has to go after C instead, wasting the hole permanently Step 1 — allocate A, B, C A B C free Step 2 — free(B) A freed hole C free Step 3 — malloc(D), D bigger than the hole A hole —too small C D free
The B-sized hole is never reused for anything bigger than B — on a device that runs for months without a reboot, this pattern compounds until an allocation fails despite plenty of total free memory.

The four classic bugs

Memory leak
Losing every pointer to an allocated block without calling free() — the memory is unreachable but the allocator still considers it in use. On a device with no process model to reclaim memory when it exits, a leak means the device slowly runs out until it's reset.
Double free
Calling free() on the same pointer twice — corrupts the allocator's internal bookkeeping, often not crashing at the point of the mistake but at some unrelated later allocation.
Use-after-free
Dereferencing a pointer after its memory has been freed — the memory may already belong to a different, unrelated allocation, so the bug's symptoms depend entirely on what else happens to run next.
Heap buffer overflow
Writing past the end of an allocated block corrupts whatever comes after it in the heap — often another allocation's data or the allocator's own metadata, producing a crash far from the actual bug.

Why embedded code avoids malloc

Alternatives: static allocation and memory pools

The simplest alternative is a compile-time-sized array — its memory footprint is fixed and known before the program ever runs. When something genuinely needs to be allocated and released at runtime, a memory pool (fixed-size block allocator) is the standard embedded answer: pre-allocate N fixed-size blocks once at startup, and "allocate"/"free" become O(1) operations that just mark a pool slot used or free — no search, no fragmentation, because every block is the same size. A pool's free list is, not coincidentally, usually implemented as the exact singly linked list covered in Linked Lists, threaded through the unused blocks themselves.

Real-world applications

Gotchas

Field notes