Memory & Lifetime
Where variables live, who owns them, and when they die. C gives you three kinds of storage — and no garbage collector to clean up after you. Every leak, double-free, and dangling pointer is a lifetime bug.
3.1 Three kinds of storage duration
| Duration | Where | Lifetime | Typical use |
|---|---|---|---|
| automatic | Stack | Until the enclosing block exits | Locals, parameters, loop variables |
| static | Data / BSS segment | Whole program run | Globals, static locals, file-scope state |
| allocated | Heap | From malloc until free | Variable-size buffers, objects outliving a call |
int g_count = 0; /* static duration: zero-initialised, whole run */
static int s_calls = 0; /* static + internal linkage: this file only */
void f(void) {
int x = 5; /* automatic: new x on every call, gone on return */
static int kept = 0; /* static local: one instance, keeps value */
kept++;
}- Stack is fast and automatic — and small. A typical embedded stack is a few KB. A
uint8_t buf[4096];local can overflow it silently. Large buffers belong in static storage or on the heap, not on the stack. - Uninitialised automatics hold garbage.
int x; printf("%d", x);is undefined behavior. Globals andstaticvariables are zero-initialised for you; locals are not. - Recursion depth is a stack budget. Each call consumes a frame. Deep or unbounded recursion on an MCU is a stack-overflow crash waiting for the worst input.
3.2 Heap: malloc / free and the ownership rule
#include <stdlib.h>
uint8_t *buf = malloc(64); /* ask for 64 bytes */
if (buf == NULL) { return ERR_NOMEM; } /* ALWAYS check: malloc can fail */
buf[0] = 0xA5; /* use it */
free(buf); /* give it back, exactly once */
buf = NULL; /* NULL it — kills the dangling pointer */The ownership rule: every allocation has exactly one owner at a time, and the owner is responsible for exactly one free. If ownership moves (a function returns a pointer, a struct takes a buffer), document the transfer in the function’s contract: “caller owns the returned pointer and must free it”.
🧱 Heap timeline
Try: allocate A, allocate B, free A, then free A again. Then reset and notice what happens if you allocate A twice without freeing — the first block becomes unreachable: that is a leak.
3.3 The five lifetime bugs (and how to catch each)
| Bug | What happens | Detection |
|---|---|---|
| Leak | Allocated memory never freed; heap shrinks until malloc fails | Valgrind, AddressSanitizer LeakSanitizer, heap watermark checks |
| Double-free | Same block freed twice; heap metadata corrupted | ASan, free + NULL habit, ownership discipline |
| Use-after-free | Pointer used after its block was returned | ASan, NULL-after-free, short lifetimes |
| Dangling (stack) | Pointer to a local that has gone out of scope | -Wreturn-local-addr, never return &local |
| Buffer overrun | Write past the allocation corrupts neighbours | ASan, bounded functions, length checks at the boundary |
int *p = malloc(sizeof *p); /* 1. leak if we return early below */
if (cond) return -1; /* p is lost — no free() on this path */
free(p);
free(p); /* 2. double-free */
*p = 7; /* 3. use-after-free */
int *q = NULL;
{ int tmp = 9; q = &tmp; } /* 4. q dangles: tmp is gone */
*q = 1; /* undefined behavior */Run your code under AddressSanitizer while developing: gcc -fsanitize=address,undefined -g. It catches most of these bugs the first time they execute, with a stack trace. It is not for shipping firmware — it is for finding bugs before you ship.
3.4 Safer patterns: init/destroy pairs and static pools
/* Every Create has a Destroy. Every Init has a Deinit.
The destroy function undoes EXACTLY what create did, in reverse. */
Sensor *Sensor_Create(void) { /* malloc + init, or claim pool slot */ }
void Sensor_Destroy(Sensor *s) { /* release everything Create acquired */ }
/* Embedded alternative: no heap at all — fixed pool, deterministic. */
#define SENSOR_MAX 4
static Sensor pool[SENSOR_MAX];
static bool pool_used[SENSOR_MAX];
/* Create claims a free slot; Destroy marks it free. No fragmentation,
no malloc failure at 3 a.m., worst-case memory known at compile time. */calloczeroes;mallocdoes not.calloc(n, sizeof *p)also makes the element count explicit.realloc: assign to a temporary —void *tmp = realloc(p, n); if (tmp) p = tmp;— because on failurereallocreturnsNULLand the original block is still yours.sizeof *poversizeof(type):malloc(sizeof *p)stays correct ifp’s type changes;malloc(sizeof(int))silently does not.- RAII does not exist in C. The discipline is manual: every acquisition path must have a release path, including every early-return and error branch. Review error paths first — that is where leaks live.