Stage 3

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

DurationWhereLifetimeTypical use
automaticStackUntil the enclosing block exitsLocals, parameters, loop variables
staticData / BSS segmentWhole program runGlobals, static locals, file-scope state
allocatedHeapFrom malloc until freeVariable-size buffers, objects outliving a call
storage.c
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 and static variables 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

heap.c — the complete pattern
#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)

BugWhat happensDetection
LeakAllocated memory never freed; heap shrinks until malloc failsValgrind, AddressSanitizer LeakSanitizer, heap watermark checks
Double-freeSame block freed twice; heap metadata corruptedASan, free + NULL habit, ownership discipline
Use-after-freePointer used after its block was returnedASan, NULL-after-free, short lifetimes
Dangling (stack)Pointer to a local that has gone out of scope-Wreturn-local-addr, never return &local
Buffer overrunWrite past the allocation corrupts neighboursASan, bounded functions, length checks at the boundary
lifetime-bugs.c — spot all four
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

init-destroy.c — symmetry is the contract
/* 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. */
  • calloc zeroes; malloc does 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 failure realloc returns NULL and the original block is still yours.
  • sizeof *p over sizeof(type): malloc(sizeof *p) stays correct if p’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.