../

C memory management & pointers

How C programs use memory, for readers of C fundamentals: where variables live, what a pointer is, pointer arithmetic, malloc and friends, ownership and lifetimes, struct layout, the classic memory bugs with the tools that catch them, and the allocation patterns real C code is built on. C23 throughout, with notes where C17 and C11 differ. C++ automates most of this with RAII and smart pointers: see C++ fundamentals.

Process memory layout

A running program sees a virtual address space: addresses that the CPU and kernel map, page by page (typically 4 KiB), onto physical memory. Only mapped pages can be touched; anything else faults with a segmentation fault. On 64-bit Linux the layout looks like this:

higher addresses lower addresses stack grows down memory-mapped grows up heap .bss .data .rodata .text null page 0x7ffc52d17cf8 0x7f… 0x562b178ab2a0 0x562ae0bfb03c 0x562ae0bfb030 0x562ae0bf9004 locals, arguments, return addresses int local = 5; gone on return shared libraries (libc.so), large malloc blocks, mmap malloc, calloc, realloc lives until free() int counter; static int calls; int limit = 10; the bytes of "hi", const globals machine code: main() and the rest address 0: dereferencing null faults
Typical x86-64 Linux layout; ASLR moves each region to a random base on every run.
RegionHoldsLifetimeNotes
.textmachine codewhole runread-only and executable
.rodatastring literals, const globalswhole runread-only: writing faults
.dataglobals and static variables with an initializerwhole runcopied from the executable file
.bssglobals and static variables without onewhole runzero-filled at startup; takes no space in the file
heapmalloc, calloc, realloc blocksuntil freegrows toward higher addresses
memory-mappedshared libraries, large malloc blocks, mmapuntil unmappedglibc serves big requests (≈128 KiB and up by default) with mmap
stacklocals, parameters, return addressesuntil the function returnstypically 8 MiB for the main thread on Linux and macOS, 1 MiB on Windows
where.c
#include <stdio.h>
#include <stdlib.h>
 
const char *greeting = "hi";  // the pointer: .data
                              // the bytes "hi": .rodata
int limit = 10;               // .data (initialized)
int counter;                  // .bss (zero-initialized)
 
int main(void) {
    static int calls;         // .bss too: not on the stack
    int local = 5;            // stack
    int *heap = malloc(sizeof *heap);   // heap
    if (heap == nullptr) return 1;
 
    printf("rodata %p\n", (void *)greeting);
    printf("data   %p\n", (void *)&limit);
    printf("bss    %p %p\n", (void *)&counter,
           (void *)&calls);
    printf("heap   %p\n", (void *)heap);
    printf("stack  %p\n", (void *)&local);
    free(heap);
}
$ ./where
rodata 0x562ae0bf9004
data   0x562ae0bfb030
bss    0x562ae0bfb03c 0x562ae0bfb038
heap   0x562b178ab2a0
stack  0x7ffc52d17cf8
$ size ./where
   text    data     bss     dec     hex filename
   1978     640      16    2634     a4a ./where

The C standard does not talk about segments; it defines storage durations:

Storage durationObjectsLifetimeInitial value
staticglobals, static locals, string literalsthe whole runzero unless initialized
automaticlocals and parametersuntil their block endsindeterminate unless initialized
allocatedmalloc family blocksuntil freeindeterminate (calloc: zero)
threadthread_local variablesuntil their thread endszero unless initialized

What a pointer is

A pointer is a variable that holds a memory address, plus a type that says what lives at that address. &x (address-of) gives a pointer to x; *p (dereference) is the object p points to, usable for reading and writing.

#include <stdio.h>
 
int main(void) {
    int x = 5;
    int *p = &x;      // p holds the address of x
    *p = 7;           // write through p: x is now 7
    int **pp = &p;    // a pointer to a pointer
 
    printf("%d %d %d\n", x, *p, **pp);   // 7 7 7
    printf("%p %p\n", (void *)&x, (void *)p);  // equal
    printf("%zu\n", sizeof p);   // 8 on 64-bit systems
}
int x = 5; int *p = &x; *p = 7; int **pp = &p; int x int *p int **pp 5 7 0x7ffc…a4c 0x7ffc…a40 at 0x7ffc…a4c at 0x7ffc…a40 at 0x7ffc…a38 x: 4 bytes on the stack holding 5 p == &x, so *p is another name for x writing *p changed x without naming it *pp == p and **pp == x == 7 example addresses · sizeof x == 4, sizeof p == sizeof pp == 8

1/4

int x = 5; reserves 4 bytes on the stack at some address, here 0x7ffc…a4c, and stores 5 there.

ExpressionMeaning
&xaddress of x; its type is "pointer to the type of x"
*pthe object at the address in p
p->mmember m of the struct p points to: (*p).m
p[i]the i-th object after the one p points to: *(p + i)
p == qsame address? (does not compare the pointed-to values)
if (p), !pnon-null? null?
sizeof p vs sizeof *psize of the pointer (8) vs size of what it points to

Read declarations from the name outward: right first, then left, with parentheses grouping.

Declarationx is
int *xa pointer to int
int *x, y;x a pointer, but y a plain int: the * binds to the name
int **xa pointer to a pointer to int
int *x[10]an array of 10 pointers to int
int (*x)[10]a pointer to an array of 10 int
int *x(void)a function returning a pointer to int
int (*x)(int)a pointer to a function taking int and returning int
const char *xa pointer to const char
char *const xa const pointer to char

A null pointer points to no object. Dereferencing it is undefined behavior; in practice the null page is unmapped and the program crashes.

NULLnullptr (C23)
What it isa macro: ((void *)0) or plain 0, depending on the librarya keyword of type nullptr_t
Converts toany pointer; if it is 0, also any integerany pointer and bool, nothing else
As a variadic argument (execl(…, NULL))wrong if it is an int 0 and pointers are wideralways passed as a null pointer

Static pointers start out null; local pointers start out with garbage (a wild pointer), so initialize them, to nullptr if there is nothing better.

Pointer arithmetic and arrays

Adding an integer to a pointer moves it by that many elements, not bytes: p + 1 is sizeof *p bytes further on. That is why the pointer's type matters.

a, p p + 2 a + 5 &a[0] &a[2] one past the end 10 20 30 40 50 no element a[0] a[1] a[2] a[3] a[4] 0x7ffc…a0 0x7ffc…a4 0x7ffc…a8 0x7ffc…ac 0x7ffc…b0 0x7ffc…b4 p + 1 moves one int: 4 bytes a[i] is *(a + i) · a + 5 may be compared, never dereferenced
a[i] is *(a + i): index arithmetic is pointer arithmetic.
#include <stddef.h>
#include <stdio.h>
 
int main(void) {
    int a[5] = {10, 20, 30, 40, 50};
    int *p = a;              // decays: same as &a[0]
 
    printf("%d %d %d\n", p[2], *(p + 2), a[2]);  // 30 x 3
    printf("%p %p\n", (void *)p, (void *)(p + 1));
    // the two addresses differ by sizeof(int) == 4
 
    ptrdiff_t d = &a[4] - &a[1];     // 3 elements
    printf("%td\n", d);
 
    for (int *it = a; it != a + 5; it++)  // a + 5 is one
        printf("%d ", *it);               // past the end
    printf("\n%zu %zu\n", sizeof a, sizeof p);  // 20 8
}

An array is not a pointer, but in almost every expression its name decays into a pointer to its first element:

ExpressionType and value
a (most uses)int *, pointing at a[0]
sizeof a20: the whole array (no decay)
&aint (*)[5]: the same address, but &a + 1 jumps 20 bytes
sizeof p8: just the pointer
a parameter int a[] or int a[5]really int *a; sizeof a inside the function is 8, and GCC and Clang warn (-Wsizeof-array-argument)
a = p;does not compile: arrays cannot be assigned
"abc"char[4], decaying to char *
#include <stddef.h>
 
#define ARRAY_LEN(a) (sizeof (a) / sizeof (a)[0])
 
// The parameter is really `const int *xs`: pass the length
long sum(const int xs[], size_t n) {
    long total = 0;
    for (size_t i = 0; i < n; i++) total += xs[i];
    return total;
}
 
long sum_all(void) {
    static const int xs[] = {1, 2, 3, 4};
    return sum(xs, ARRAY_LEN(xs));  // ARRAY_LEN on the real
}                                   // array, never on xs[]

Rules for pointer arithmetic:

  • It is defined only inside one array and up to one past its end. Computing a + 6 or a - 1 is undefined even if you never dereference it; a + 5 may be computed and compared but not dereferenced.
  • Subtracting pointers gives a ptrdiff_t count of elements; both must point into the same array.
  • == and != work on any two pointers; <, > only within one array.
  • Arithmetic on void * is a GNU extension (-pedantic warns): cast to unsigned char * for byte offsets.
  • A single object counts as an array of one, so &x + 1 is a valid one-past-the-end pointer.
char s[] = "hi";const char *s = "hi";
What it isa 3-byte array holding a copy of the literala pointer to the literal itself
Writableyes: s[0] = 'H';no: writing is undefined (use const so the compiler stops you)
sizeof s38
s = other;does not compilefine: the pointer moves

const and pointers

const applies to whatever is immediately to its left (or to its right when it comes first). With pointers there are two things that can be read-only: the pointed-to object and the pointer itself.

DeclarationChange *p?Change p?Read as
const int *p (same as int const *p)noyespointer to const int
int *const pyesnoconst pointer to int
const int *const pnonoconst pointer to const int
const int *a = &x; pointer to const int a &x x ✓ a = &y; a may move ✗ *a = 5; read-only via a int *const b = &x; const pointer to int b &x x ✓ *b = 5; writes x ✗ b = &y; b is fixed const int *const c = &x; const pointer to const int c &x x ✗ c = &y; c is fixed ✗ *c = 5; read-only via c lock on the arrow: no writes through it · lock on the box: the pointer cannot move const left of * protects the int, right of * the pointer · x itself stays writable
const before the * protects what the pointer points to; const after it protects the pointer itself.
#include <stdio.h>
 
int main(void) {
    int x = 1, y = 2;
 
    const int *a = &x;        // pointer to const int
    // *a = 5;                // error: *a is read-only
    a = &y;                   // fine: a itself can move
 
    int *const b = &x;        // const pointer to int
    *b = 5;                   // fine
    // b = &y;                // error: b is read-only
 
    const int *const c = &x;  // neither can change
    printf("%d %d %d\n", *a, *b, *c);   // 2 5 5
}
  • Put const on pointer parameters a function only reads through: size_t strlen(const char *s). It documents the contract and lets callers pass read-only data.
  • int * converts to const int * implicitly (adding a promise is safe). Going back needs a cast, and writing through it to an object that was defined const is undefined behavior.
  • The conversion is only automatic at the first level: char ** does not convert to const char **, nor (in C, unlike C++) to const char *const *. Declare the right type, or cast.
  • const is shallow: a const struct with a char *name member still lets you modify name[0].

Pointers to pointers and 2-D arrays

A pointer to a pointer lets a function change the caller's pointer: return a new allocation through an out-parameter, or unlink a node in a list (see the linked list recipe).

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
// On success sets *out to a new string (caller frees) and
// returns 0; on failure leaves *out alone, returns -1.
int join_path(const char *dir, const char *file,
              char **out) {
    size_t n = strlen(dir) + 1 + strlen(file) + 1;
    char *s = malloc(n);
    if (!s) return -1;
    snprintf(s, n, "%s/%s", dir, file);
    *out = s;
    return 0;
}
 
int main(void) {
    char *path = nullptr;
    if (join_path("/etc", "hosts", &path) != 0) return 1;
    puts(path);                  // /etc/hosts
    free(path);
}

Four ways to store a grid of rows × cols numbers:

FormMemoryIndexingFreeing
double m[3][4]one contiguous block, row after row (row-major)m[r][c]automatic (stack) or static
double (*m)[cols] = malloc(rows * sizeof *m)one heap block; m points to rows of colsm[r][c]one free(m)
double *m = malloc(rows * cols * sizeof *m)one heap blockm[r * cols + c]one free(m)
double **m: an array of row pointersrows + 1 separate blocks; rows may differ in length (jagged)m[r][c]free each row, then m
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
int main(void) {
    size_t rows = 3, cols = 4;
 
    // Contiguous: one allocation; the type knows cols
    double (*m)[cols] = malloc(rows * sizeof *m);
    if (!m) return 1;
    for (size_t r = 0; r < rows; r++)
        for (size_t c = 0; c < cols; c++)
            m[r][c] = (double)(r * cols + c);
    printf("%.0f\n", m[2][3]);       // 11
    free(m);
 
    // Jagged: an array of pointers, each row its own block
    const char *words[] = {"ab", "cde", "f"};
    char **rowsv = calloc(3, sizeof *rowsv);
    if (!rowsv) return 1;
    for (size_t i = 0; i < 3; i++)
        rowsv[i] = strdup(words[i]);   // null on failure
    printf("%s\n", rowsv[1] ? rowsv[1] : "?");   // cde
    for (size_t i = 0; i < 3; i++) free(rowsv[i]);
    free(rowsv);
}

double (*m)[cols] is a pointer to a variable-length array type. GCC and Clang support it, and C23 makes such types mandatory again (while VLAs on the stack stay optional); MSVC supports neither, so portable code uses the flat m[r * cols + c] form (matrix recipe). A double[3][4] is not a double **: passing one where the other is expected is a type error, because there are no row pointers in a contiguous block.

double (*m)[cols] = malloc(rows * sizeof *m); 1 block m 0 1 2 3 4 5 6 7 8 9 10 11 m[0] m[1] m[2] byte 0 32 64 96 m[2][3] is byte (2 * 4 + 3) * 8 = 88 of the block: arithmetic, no pointers stored char **rowsv: calloc(3, sizeof *rowsv), then strdup per row 4 blocks rowsv rowsv[0] a b \0 rowsv[1] c d e \0 rowsv[2] f \0 8-byte pointers separate blocks, any length rowsv[1][2] loads a pointer, then a char · free each row, then rowsv so a double[3][4] is not a double **: the block above has no row pointers in it
A contiguous 2-D array is arithmetic on one block; row pointers mean several blocks and an extra load per access.

Function pointers and callbacks

A function's name decays to a pointer to it, and calling through the pointer looks like a normal call. This is how C does callbacks, plugins, strategy objects and dispatch tables.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
typedef struct { const char *name; int age; } Person;
 
// qsort calls this with pointers to two elements
static int by_age(const void *a, const void *b) {
    const Person *x = a, *y = b;
    return (x->age > y->age) - (x->age < y->age);
}
 
static int by_name(const void *a, const void *b) {
    const Person *x = a, *y = b;
    return strcmp(x->name, y->name);
}
 
int main(void) {
    Person people[] = {{"Lin", 41}, {"Ada", 36}, {"Bo", 29}};
    size_t n = sizeof people / sizeof people[0];
 
    int (*cmp)(const void *, const void *) = by_age;
    qsort(people, n, sizeof people[0], cmp);
    printf("%s\n", people[0].name);   // Bo
 
    qsort(people, n, sizeof people[0], by_name);
    printf("%s\n", people[0].name);   // Ada
}

Write the comparison as (x > y) - (x < y), not x - y, which overflows for large values.

Callbacks take a void * context argument so the caller can pass its own state through code that knows nothing about it:

#include <stddef.h>
#include <stdio.h>
 
typedef void Visit(int value, void *ctx);  // function type
 
static void for_each(const int *xs, size_t n, Visit *f,
                     void *ctx) {
    for (size_t i = 0; i < n; i++) f(xs[i], ctx);
}
 
static void add_to(int v, void *ctx) {
    *(long *)ctx += v;
}
 
typedef double BinOp(double, double);
static double add(double a, double b) { return a + b; }
static double mul(double a, double b) { return a * b; }
 
static const struct { char sym; BinOp *fn; } ops[] = {
    {'+', add},
    {'*', mul},
};
 
int main(void) {
    int xs[] = {1, 2, 3, 4};
    long total = 0;
    for_each(xs, 4, add_to, &total);
    printf("%ld\n", total);                  // 10
 
    for (size_t i = 0; i < 2; i++)     // dispatch table
        printf("6 %c 7 = %g\n", ops[i].sym, ops[i].fn(6, 7));
}
SyntaxMeaning
int (*f)(int)f is a pointer to a function taking and returning int
typedef int Fn(int); Fn *f;the same, easier to read; typedef the function type
f(3), (*f)(3)both call it
f = name; or f = &name;both store the address
calling through the wrong typeundefined behavior: the signatures must match exactly
function pointer ↔ void *not allowed in ISO C (POSIX dlsym requires it to work anyway)

void pointers and casting

void * is the generic object pointer: any object pointer converts to it and back without a cast and without losing information. It is how malloc, memcpy, qsort and callbacks stay type-agnostic. You cannot dereference it or do arithmetic on it; convert it to a typed pointer first.

#include <stdio.h>
#include <string.h>
 
// Works for any type: the caller supplies the size
void swap_any(void *a, void *b, size_t size) {
    unsigned char *x = a, *y = b;   // byte view
    for (size_t i = 0; i < size; i++) {
        unsigned char t = x[i];
        x[i] = y[i];
        y[i] = t;
    }
}
 
int main(void) {
    double p = 1.5, q = 2.5;
    swap_any(&p, &q, sizeof p);
    char s[8] = "left", t[8] = "right";
    swap_any(s, t, sizeof s);
    printf("%g %g %s %s\n", p, q, s, t);
}   // 2.5 1.5 right left
ConversionRule
T * → void * → T *implicit both ways in C; gives back the original pointer
malloc resultdo not cast it in C: int *p = malloc(n * sizeof *p); (C++ requires the cast)
any object → unsigned char *always allowed: inspect or copy the bytes of anything
T * → unrelated U *, then dereferenceusually undefined behavior (strict aliasing, alignment): use memcpy (below)
pointer ↔ integeruintptr_t u = (uintptr_t)p; for hashing or alignment math; implementation-defined
casting away constcompiles; writing to a truly const object is UB

Dynamic allocation

FunctionDoesOn failureNotes
malloc(size)allocates size bytes, uninitializedreturns nullmalloc(0) returns null or a unique pointer; free either
calloc(n, size)allocates n * size bytes, zeroedreturns nullchecks n * size for overflow (required since C23)
realloc(p, size)resizes: may grow in place or move, copying the old bytes; new bytes uninitializedreturns null, old block untouchedrealloc(nullptr, n) is malloc(n); realloc(p, 0) is UB in C23
free(p)releases a block from the functions abovefree(nullptr) does nothing; p now dangles
aligned_alloc(align, size)like malloc with a stronger alignment (C11)returns nullfree with free; keep size a multiple of align
free_sized(p, size)free with a size hint (C23)glibc 2.43+
#include <stdio.h>
#include <stdlib.h>
 
typedef struct { double x, y; } Point;
 
int main(void) {
    size_t n = 1000;
    Point *pts = malloc(n * sizeof *pts);  // type named once
    if (pts == nullptr) {
        perror("malloc");
        return 1;
    }
    for (size_t i = 0; i < n; i++)
        pts[i] = (Point){.x = (double)i, .y = 0.0};
 
    int *counts = calloc(256, sizeof *counts);  // all zero
    if (!counts) { free(pts); return 1; }
 
    // Grow: keep the old pointer until realloc succeeds
    size_t bigger = n * 2;
    Point *tmp = realloc(pts, bigger * sizeof *pts);
    if (!tmp) { free(counts); free(pts); return 1; }
    pts = tmp;               // the block may have moved
    n = bigger;
 
    printf("%.0f %d %zu\n", pts[999].x, counts[0], n);
    free(counts);
    free(pts);
    pts = nullptr;           // a later use fails loudly
}
IdiomWhy
p = malloc(n * sizeof *p);the element type is written once; changing p's type cannot leave a stale sizeof(OldType)
if (!p) … after every allocationnull on failure; dereferencing it is UB. On Linux, overcommit means malloc rarely fails, but huge or limited requests still do
tmp = realloc(p, …); if (tmp) p = tmp;p = realloc(p, …) loses (leaks) the old block when it returns null
check n > SIZE_MAX / size before malloc(n * size)the product can wrap around and allocate far too little
free(p); p = nullptr;turns a later use-after-free into a predictable null dereference, and a double free into a no-op
no cast on mallocvoid * converts implicitly in C; a cast only adds noise
#include <stdint.h>
#include <stdlib.h>
 
// malloc(n * size) without the silent wrap-around
void *alloc_array(size_t n, size_t size) {
    if (size != 0 && n > SIZE_MAX / size) return nullptr;
    return malloc(n * size);
}

malloc returns memory aligned for any standard type (alignof(max_align_t), 16 bytes on x86-64 Linux). Each block costs some hidden bookkeeping and the allocator rounds sizes up, so a million tiny allocations cost far more than one big one; see arenas and pools.

When a growable array runs out of room, realloc may have to move it:

stack heap xs tmp len cap 0x5555…2a0 0x5555…6c0 0x5555…6c0 0 2 3 2 4 dangling ? ? 10 20 freed by realloc free memory 0x5555…2a0 [0] [1] [2] [3] 10 20 ? 30 ? 0x5555…6c0 int *xs = malloc(2 * sizeof *xs); len = 0; cap = 2; xs[len++] = 10; xs[len++] = 20; // len == cap int *tmp = realloc(xs, 4 * sizeof *xs); if (tmp) { xs = tmp; cap = 4; } xs[len++] = 30; ? = indeterminate: never read before writing · example addresses

1/5

malloc(2 * sizeof *xs) returns an 8-byte block at 0x5555…2a0. Its contents are indeterminate (?), so len = 0 and cap = 2.

Ownership and lifetimes

C has no garbage collector and no destructors. Every allocation needs an owner: the one piece of code responsible for freeing it exactly once. Nothing enforces this, so make it visible in names, comments and function pairs.

PatternExampleWho frees
caller provides the buffersnprintf(buf, n, …), fgets(buf, n, f)the caller, often nothing to free (stack)
function returns a new allocationstrdup, make_id belowthe caller, with free or a matching thing_free
create/destroy pairfopen/fclose, counter_new/counter_freethe caller, through the destroy function
borrowed pointera const char *name parameternobody; the callee must not keep it after returning
pointer into static storagestrerror, getenv, asctimenobody; overwritten by the next call, not thread-safe
ownership transferred in"list_add takes ownership of item"the receiver, from then on
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
// 1. Caller provides the buffer: no allocation at all
bool format_id(char *buf, size_t cap, int id) {
    int n = snprintf(buf, cap, "user-%04d", id);
    return n >= 0 && (size_t)n < cap;
}
 
// 2. Callee allocates; the comment says who frees.
// Returns a new string, or null. Caller must free() it.
char *make_id(int id) {
    char tmp[32];
    if (!format_id(tmp, sizeof tmp, id)) return nullptr;
    return strdup(tmp);          // C23 (POSIX before)
}
 
// 3. Static buffer: nothing to free, but the next call
//    overwrites it and threads would share it
const char *id_str(int id) {
    static char buf[32];
    return format_id(buf, sizeof buf, id) ? buf : "?";
}
 
int main(void) {
    char a[16];
    if (format_id(a, sizeof a, 7)) puts(a);   // user-0007
    char *b = make_id(8);
    if (b) { puts(b); free(b); }             // user-0008
    puts(id_str(9));                         // user-0009
}

What you must never do is return the address of a local: char *f(void) { char buf[32] = "hi"; return buf; } compiles (with a warning) and hands the caller a pointer into a stack frame that no longer exists.

stack: higher addresses at the top running now rsp rsp rsp rsp rsp rsp rsp main ret → libc p ? p 0x7ffc…d9c y ? y 9 bad ret → main bad ret → main square ret → main square ret → main x ? x 36 x 36 n 3 n 3 square ret → bad square ret → bad r 9 r 9 n 6 n 6 r 36 r 36 ← p points here ← p: now n ← *p: undefined int square(int n) { int r = n * n; return r; } int *bad(void) { int x = square(6); return &x; // BUG } int main(void) { int *p = bad(); int y = square(3); return *p + y; // UB } main's frame: a return address, then its locals call bad(): push a return address, then bad's x square(6) runs three frames deep; rsp is the top square returned: its frame is popped, the bytes stay bad returned &x: p now points into a dead frame square(3) reuses those bytes: x's slot is now n *p reads whatever is left there: undefined behavior simplified: real frames also save registers and add padding · example address

1/7

main starts. Its frame holds a return address (where to continue when main ends) and its locals p and y, still indeterminate. rsp, the stack pointer register, marks the top of the stack, which grows toward lower addresses.

A frame lives only until its function returns; the next call reuses the same bytes.
ObjectIts lifetime endsA pointer to it afterward
local variable or parameterat the } of its blockdangles
compound literal inside a functionat the end of the enclosing blockdangles
string literalnevervalid, but read-only
static localnevervalid; shared by every call
malloc'd blockat free, or at a realloc that moves itdangles
an element of a dynamic arraywhen the array is reallocatedtreat it as dangling, even if the block happened not to move this time
Use the stack whenUse the heap when
the size is fixed and small (up to a few KiB)the size is only known at runtime, or large
the object is only needed during this callit must outlive the function that creates it
speed matters: allocation is freeit is shared between objects, threads or modules
(many same-sized objects: a pool)(many objects with one lifetime: an arena)

Strings and buffers safely

A C string needs strlen(s) + 1 bytes: the '\0' is not counted by strlen but must be stored. sizeof gives an array's capacity, but only for an array in scope; on a char * it gives 8.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
int main(void) {
    char buf[32] = "hello";
    const char *p = buf;
    printf("%zu %zu %zu\n", strlen(buf), sizeof buf,
           sizeof p);                       // 5 32 8
 
    size_t n = strlen(buf) + 1;             // + 1 for '\0'
    char *copy = malloc(n);
    if (!copy) return 1;
    memcpy(copy, buf, n);
 
    // Bounded append: snprintf never writes past the cap
    size_t len = strlen(buf);
    snprintf(buf + len, sizeof buf - len, ", %s", "world");
    printf("%s / %s\n", buf, copy);   // hello, world / hello
    free(copy);
}

strncpy looks like a safe strcpy but is not: strncpy(d, "hello", 4) writes h e l l and no terminator, and when the source is short it pads the rest of d with zeros. It was designed for fixed-width record fields. A copy that always terminates and reports truncation:

#include <string.h>
 
// Copy src into dst[cap] (cap > 0), always terminated.
// Returns false if src was too long and got truncated.
bool str_copy(char *dst, size_t cap, const char *src) {
    size_t n = strlen(src);
    if (n >= cap) {
        memcpy(dst, src, cap - 1);
        dst[cap - 1] = '\0';
        return false;
    }
    memcpy(dst, src, n + 1);
    return true;
}
Instead ofUseWhy
gets(buf)fgets(buf, sizeof buf, stdin)gets cannot be used safely; removed in C11
strcpy(d, s)str_copy above, snprintf(d, cap, "%s", s), or check strlen then memcpyno size limit
strcat(d, s)track the length; snprintf(d + len, cap - len, …)no size limit, O(n) to find the end
strncpy(d, s, n)str_copy, or strlcpy where available (BSDs, macOS, glibc 2.38+, POSIX.1-2024)may leave d unterminated
sprintf(buf, …)snprintf(buf, sizeof buf, …)no size limit
scanf("%s", w)scanf("%63s", w) or fgetsno size limit
atoi(s)strtol (recipe)no error reporting; UB on overflow
strtok(s, ",")strtok_r (POSIX), or scan with strcspnhidden global state
memset(key, 0, n) before free(key)memset_explicit (C23; glibc 2.43+) or explicit_bzerothe optimizer may delete a memset of memory that is about to die

Bytes are not characters: strlen counts bytes, so a UTF-8 string with accents is longer than it looks, and cutting at an arbitrary byte can split a character. For binary data that may contain zero bytes, carry a pointer and a length instead of relying on '\0'.

Structs in memory

Each member must sit at an address that is a multiple of its alignment (alignof): 1 for char, 4 for int, 8 for double and pointers on 64-bit platforms. To get there, the compiler inserts padding bytes between members and at the end.

#include <stddef.h>   // offsetof
#include <stdio.h>
 
struct item {          // offset  size
    char tag;          //   0      1   then 7 bytes padding
    double price;      //   8      8
    int qty;           //  16      4   then 4 bytes padding
};                     // sizeof 24, alignof 8
 
struct item_sorted {   // the same fields, largest first
    double price;      //   0      8
    int qty;           //   8      4
    char tag;          //  12      1   then 3 bytes padding
};                     // sizeof 16
 
int main(void) {
    printf("%zu %zu %zu %zu\n", sizeof(struct item),
           offsetof(struct item, qty), alignof(struct item),
           sizeof(struct item_sorted));   // 24 16 8 16
}
struct item { char tag; double price; int qty; } sizeof 24 tag 7 bytes padding double price int qty tail pad 0 8 16 24 struct item_sorted { double price; int qty; char tag; } sizeof 16 double price int qty tag pad 8 bytes saved 0 8 16 each member starts at a multiple of its alignment: double 8, int 4, char 1 sizeof rounds up to a multiple of the largest one (8), so arrays stay aligned
Same three fields, a third less memory: sort members from largest to smallest alignment.
RuleDetail
member offseta multiple of the member's alignment
struct alignmentthe largest alignment of any member
sizeofrounded up to a multiple of the struct's alignment (tail padding), so every element of an array is aligned
ordermembers stay in declaration order; the compiler never reorders them, but you can
padding contentsunspecified: do not memcmp structs or fwrite them to files and sockets
offsetof(T, m)byte offset of m in T (<stddef.h>)
-Wpaddedwarns wherever padding is inserted; pahole (from the dwarves tools) prints the holes of compiled structs
packing[[gnu::packed]] (GCC/Clang) or #pragma pack(1) removes padding; misaligned members are slower and taking their address is unsafe on some CPUs

For file formats and network protocols, serialize field by field with an explicit byte order rather than writing a struct's bytes: padding, alignment and endianness differ between platforms.

Common memory bugs

BugTiny exampleWhat happensCaught by
leakp = malloc(64); p = malloc(64);the first block can never be freed; memory use growsLeakSanitizer (part of ASan on Linux), Valgrind
double freefree(p); free(p);allocator corruption; glibc often aborts with free(): double free detectedASan, Valgrind, -fanalyzer
use after freefree(n); n = n->next;reads or overwrites memory now used by something elseASan, Valgrind
dangling pointer to a localint *f(void) { int x = 1; return &x; }garbage values or a crash much latercompiler warning (on by default), ASan (stack-use-after-return)
heap buffer overflowchar *s = malloc(5); strcpy(s, "hello");overwrites the allocator's bookkeeping or a neighbor blockASan, Valgrind, -D_FORTIFY_SOURCE=2 with -O2
stack buffer overflowchar b[8]; b[8] = 0;overwrites other locals or the return addressASan, -fstack-protector-strong; Valgrind misses most
off-by-onefor (i = 0; i <= n; i++) a[i] = 0;, malloc(strlen(s))one element, or the terminator, past the endASan
uninitialized readint *a = malloc(n * sizeof *a); s += a[0];random values that change between runs and buildsValgrind (--track-origins=yes), MemorySanitizer; -Wall for locals
invalid freefree(&x), free(p + 1), free("literal")the allocator aborts or corrupts its stateASan, Valgrind, -Wfree-nonheap-object
realloc leakp = realloc(p, n); when it failsthe old block is lostcode review: realloc into a temporary
null dereferenceusing malloc's result uncheckedsegmentation fault-fanalyzer, UBSan
stack overflowrunaway recursion, or char big[16 << 20]; as a localsegmentation faultASan reports stack-overflow; ulimit -s shows the limit

All of these are undefined behavior: the program may appear to work, especially in a debug build, and fail later or elsewhere. Run tests under a sanitizer instead of trusting a clean run.

a d a \0 ? ? ? ? ? ? ? ? ? ? ? ? 1 2 3 4 \0 ? ? ? ? ? ? ? ? ? ? ? X 2 3 4 \0 ? ? ? ? ? ? ? ? ? ? ? free: the allocator's list link free list 0x5555…2a0 0x5555…2a0 handed out next stack heap: one 16-byte block 0x5555…2a0 name pin 0x5555…2a0 0x5555…2a0 dangling char *name = malloc(16); strcpy(name, "ada"); 16 bytes; the 12 after the string are indeterminate (?) free(name); // name keeps the old address the block is the allocator's again: it writes its own data there char *pin = malloc(16); strcpy(pin, "1234"); same size: glibc hands back the block it was just given name[0] = 'X'; // use after free no crash: pin now reads "X234" · ASan: heap-use-after-free free(name); free(name); // double free the block would be listed twice; glibc spots this case and aborts

1/5

malloc(16) returns a 16-byte block, here at 0x5555…2a0. strcpy fills 4 bytes; the other 12 are indeterminate.

A freed block goes back to the allocator, and the next allocation of that size may be the same memory.

Debugging tools

# AddressSanitizer + UBSan: rebuild with checks compiled in
cc -std=c23 -g -O1 -fno-omit-frame-pointer \
   -fsanitize=address,undefined app.c -o app
./app
 
# Valgrind memcheck: no rebuild needed, much slower
cc -std=c23 -g -O0 app.c -o app
valgrind --leak-check=full --track-origins=yes ./app
 
# GCC's static analyzer: finds bugs without running
gcc -std=c23 -fanalyzer -c app.c
 
# Clang's static analyzer over a whole build
scan-build make
ToolFindsCostNotes
AddressSanitizer (-fsanitize=address)heap, stack and global overflows, use after free, double and invalid free, use after returnabout 2× slower, several times the memoryGCC and Clang; MSVC has /fsanitize=address
LeakSanitizerblocks still allocated at exitalmost noneruns automatically with ASan on Linux; standalone with -fsanitize=leak
UndefinedBehaviorSanitizer (-fsanitize=undefined)signed overflow, bad shifts, misaligned or null access, out-of-bounds indexes into arrays of known sizesmallcombine with ASan
MemorySanitizer (-fsanitize=memory)reads of uninitialized memoryabout 3× slowerClang only; every library the program uses must be instrumented too
Valgrind memcheckheap errors, leaks, uninitialized readsabout 20–30× slowerno recompile; Linux and some other Unix systems; weak on stack arrays
-fanalyzerdouble free, leaks, null dereference, use after free along specific pathscompile timeGCC 10+; may report false positives
-Wall -Wextrareturning a local's address, sizeof on an array parameter, uninitialized localsfreealways on
-D_FORTIFY_SOURCE=2 (or 3) with -O1 or moreoverflows into buffers of known size in memcpy, strcpy, sprintftinyglibc; some distributions' GCC enable it by default

ASan and Valgrind cannot be combined. Sample reports, from GCC 13 on x86-64 Linux, trimmed:

$ ./uaf      # free(name); return name[0];
==5727==ERROR: AddressSanitizer: heap-use-after-free on address 0x502000000010 ...
READ of size 1 at 0x502000000010 thread T0
    #0 0x559481caa28d in main uaf.c:9
 
0x502000000010 is located 0 bytes inside of 8-byte region [0x502000000010,0x502000000018)
freed by thread T0 here:
    #0 0x7fc0beefc4d8 in free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:52
    #1 0x559481caa24f in main uaf.c:8
 
previously allocated by thread T0 here:
    #0 0x7fc0beefd9c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
    #1 0x559481caa23f in main uaf.c:5
 
SUMMARY: AddressSanitizer: heap-use-after-free uaf.c:9 in main
$ gcc -fanalyzer -c df.c
df.c: In function 'process':
df.c:8:5: warning: double-'free' of 'buf' [CWE-415] [-Wanalyzer-double-free]
    8 |     free(buf);
      |     ^~~~~~~~~
  'process': events 1-5
    |    4 |     int *buf = malloc(n * sizeof *buf);
    |      |                (1) allocated here
    |    5 |     if (n > 10) {
    |      |        (2) following 'true' branch (when 'n > 10')...
    |    6 |         free(buf);
    |      |         (3) ...to here
    |      |         (4) first 'free' here
    |    8 |     free(buf);
    |      |     (5) second 'free' here; first 'free' was at (4)

Read an ASan report top down: the kind of error and the faulting line, then where the memory was freed, then where it was allocated. Leak reports appear when the program exits; an example is in the leak recipe.

Patterns

goto cleanup

One exit path that frees everything acquired so far. Initialize every resource to null first; free(nullptr) is a no-op, so the cleanup block needs no flags.

#include <stdio.h>
#include <stdlib.h>
 
// Sum the bytes of a file; returns 0 on success, -1 on error
int checksum(const char *path, unsigned long *out) {
    int rc = -1;
    FILE *f = nullptr;
    unsigned char *buf = nullptr;
    unsigned long sum = 0;
    size_t n;
 
    if (!(f = fopen(path, "rb"))) goto done;
    if (!(buf = malloc(64 * 1024))) goto done;
    while ((n = fread(buf, 1, 64 * 1024, f)) > 0)
        for (size_t i = 0; i < n; i++) sum += buf[i];
    if (ferror(f)) goto done;
    *out = sum;
    rc = 0;
done:
    free(buf);              // reverse order of acquisition
    if (f) fclose(f);
    return rc;
}

Arena (bump) allocator

An arena hands out memory by bumping an offset through one big block and frees everything at once by resetting the offset. Allocation is a few instructions, there is no per-object free to forget, and everything with the same lifetime (one request, one frame, one parse) goes in the same arena.

arena.c
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
 
typedef struct {
    unsigned char *base;
    size_t cap, used;
} Arena;
 
bool arena_init(Arena *a, size_t cap) {
    a->base = malloc(cap);
    a->cap = a->base ? cap : 0;
    a->used = 0;
    return a->base != nullptr;
}
 
// align: a power of two, at most alignof(max_align_t)
void *arena_alloc(Arena *a, size_t size, size_t align) {
    size_t start = (a->used + align - 1) & ~(align - 1);
    if (start > a->cap || size > a->cap - start)
        return nullptr;                 // out of space
    a->used = start + size;
    return a->base + start;
}
 
// n * sizeof(T) can overflow: check n if it is untrusted
#define ARENA_NEW(a, T, n) \
    ((T *)arena_alloc((a), (n) * sizeof(T), alignof(T)))
 
void arena_reset(Arena *a) { a->used = 0; }   // free all
 
void arena_free(Arena *a) {
    free(a->base);
    *a = (Arena){};
}
 
int main(void) {
    Arena a;
    if (!arena_init(&a, 1 << 20)) return 1;   // 1 MiB
    for (int frame = 0; frame < 3; frame++) {
        double *xs = ARENA_NEW(&a, double, 1000);
        char *name = ARENA_NEW(&a, char, 32);
        if (!xs || !name) break;
        snprintf(name, 32, "frame %d", frame);
        xs[999] = frame;
        printf("%s: %zu bytes used\n", name, a.used);
        arena_reset(&a);      // the whole frame, freed
    }
    arena_free(&a);
}
base cap used 0x7f…010 null 1048576 0 0 8000 8032 0 1000 doubles stale bytes name free free given back with one free(base) 0 8000 8032 1048576 xs xs name name Arena a, on the stack heap block, not to scale arena_init(&a, 1 << 20); one malloc of 1 MiB; nothing handed out yet double *xs = ARENA_NEW(&a, double, 1000); start 0 is already a multiple of alignof(double), 8 char *name = ARENA_NEW(&a, char, 32); start 8000 (char needs 1); prints "frame 0: 8032 bytes used" arena_reset(&a); // used = 0 everything freed at once; old xs and name must not be used double *xs = ARENA_NEW(&a, double, 1000); // frame 1 the same bytes again: no malloc, no free arena_free(&a); one free(base), then *a = (Arena){}

1/6

arena_init(&a, 1 << 20) makes one malloc of 1 MiB (glibc serves a block this big with mmap). base points to it, cap is 1048576 and used is 0.

Allocating bumps an offset; freeing everything is one assignment.

Variations: chain a new block when one fills up instead of failing; save used and restore it later to free just the most recent allocations (a scratch arena); give each thread its own arena.

Pools and free lists

A pool serves many objects of one size from a preallocated array. Free slots are linked together through their own memory (a free list), so allocating and freeing are O(1) pointer swaps with no fragmentation. Typical uses: particles, network connections, tree nodes. A complete version is in the pool recipe.

Opaque pointers

Declare a struct in the header without its fields. Callers can hold and pass Counter * but cannot see inside or allocate one themselves, so the implementation can change without recompiling them: C's version of private members.

counter.h
#ifndef COUNTER_H
#define COUNTER_H
 
typedef struct Counter Counter;  // incomplete type
 
Counter *counter_new(const char *name);  // null on failure
void counter_add(Counter *c, long n);
long counter_total(const Counter *c);
void counter_free(Counter *c);           // accepts null
 
#endif
counter.c
#include <stdlib.h>
#include <string.h>
#include "counter.h"
 
struct Counter {           // only this file sees the fields
    char *name;
    long total;
};
 
Counter *counter_new(const char *name) {
    Counter *c = malloc(sizeof *c);
    if (!c) return nullptr;
    c->name = strdup(name);
    if (!c->name) { free(c); return nullptr; }
    c->total = 0;
    return c;
}
 
void counter_add(Counter *c, long n) { c->total += n; }
long counter_total(const Counter *c) { return c->total; }
 
void counter_free(Counter *c) {
    if (!c) return;
    free(c->name);         // free what it owns, then itself
    free(c);
}

FILE from <stdio.h> works the same way: you only ever hold a FILE *.

Flexible array members

A struct's last member may be an array with no size. Allocate the header and the data in one block: one malloc, one free, and the data sits right next to its length.

#include <stdint.h>
#include <stdlib.h>
#include <string.h>
 
typedef struct {
    size_t len;
    unsigned char bytes[];  // flexible array member (C99)
} Blob;
 
// Returns a new Blob holding a copy of src; free() it.
Blob *blob_new(const void *src, size_t len) {
    if (len > SIZE_MAX - sizeof(Blob)) return nullptr;
    Blob *b = malloc(sizeof *b + len);   // header + data
    if (!b) return nullptr;
    b->len = len;
    memcpy(b->bytes, src, len);
    return b;
}

sizeof(Blob) does not include bytes. A struct with a flexible array member cannot be an array element or a member of another struct, and copying it by value copies only the header.

Advanced qualifiers and aliasing

restrict

restrict on a pointer parameter promises that, while the function runs, the object it points to is accessed only through that pointer. The compiler can then keep values in registers and vectorize without re-reading memory after each write. Breaking the promise is undefined behavior.

#include <stddef.h>
 
// dst and src must not overlap
void axpy(size_t n, float a, float *restrict dst,
          const float *restrict src) {
    for (size_t i = 0; i < n; i++) dst[i] += a * src[i];
}

memcpy's parameters are restrict, memmove's are not:

memcpy(dst, src, n)memmove(dst, src, n)
Overlapping rangesundefined behaviorhandled correctly
Speedfastestabout the same in practice
Use forcopying between distinct buffersshifting data within one buffer: memmove(a, a + 1, (n - 1) * sizeof *a)

volatile

volatile tells the compiler that every read and write of the object must really happen, in order, because something outside the program's visible flow can change or observe it: memory-mapped hardware registers, a signal handler, setjmp/longjmp. It does not make anything thread-safe; for threads use _Atomic (<stdatomic.h>) or a mutex.

#include <signal.h>
#include <stdio.h>
 
static volatile sig_atomic_t stop = 0;
 
static void on_sigint(int) { stop = 1; }   // C23: unnamed
 
int main(void) {
    signal(SIGINT, on_sigint);
    unsigned long long ticks = 0;
    while (!stop) ticks++;        // Ctrl-C ends the loop
    printf("\nstopped after %llu ticks\n", ticks);
}

Without volatile the compiler may read stop once, see 0 and turn the loop into while (1).

Strict aliasing and type punning

The strict aliasing rule: an object may only be accessed through an lvalue of its own type (or a compatible, signed/unsigned or qualified version of it), or through a character type. Reading a float through a uint32_t * is undefined, and optimizers at -O2 rely on the rule. To reinterpret bytes, copy them:

#include <inttypes.h>
#include <stdio.h>
#include <string.h>
 
uint32_t float_bits(float f) {
    uint32_t u;
    static_assert(sizeof u == sizeof f);
    memcpy(&u, &f, sizeof u);   // defined; compiles to a
    return u;                   // single register move
    // return *(uint32_t *)&f;  // UB: strict aliasing
}
 
int main(void) {
    printf("%08" PRIX32 "\n", float_bits(1.0f)); // 3F800000
}
TechniqueStatus in C
memcpy into a variable of the other typealways defined; the compiler optimizes the call away
reading another member of a union (union { float f; uint32_t u; })defined in C (it reinterprets the bytes); undefined in C++
unsigned char * view of any objectalways allowed
casting a pointer and dereferencing itundefined unless the types are compatible
-fno-strict-aliasingturns the optimization off (the Linux kernel builds with it)

alignas, alignof and aligned_alloc

#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
 
typedef struct {
    alignas(64) long hits;   // its own 64-byte cache line
} PerThreadCounter;
 
int main(void) {
    printf("%zu %zu\n", alignof(max_align_t),
           sizeof(PerThreadCounter));          // 16 64
 
    // 64-byte aligned, for SIMD or to avoid false sharing
    void *p = aligned_alloc(64, 64 * 16);  // size: k * 64
    if (!p) return 1;
    printf("%d\n", (uintptr_t)p % 64 == 0);    // 1
    free(p);
}

alignas, alignof and static_assert are keywords in C23; C11 spells them _Alignas, _Alignof and _Static_assert (or includes <stdalign.h> and <assert.h>). Alignment must be a power of two. A pointer converted to a type with stricter alignment than its address has, then dereferenced, is undefined behavior (UBSan's alignment check catches it).

Recipes

A safe growable vector

The workhorse dynamic array: amortized O(1) push by doubling, overflow-checked, never leaking on failure.

vec.c
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
 
typedef struct {
    int *data;
    size_t len, cap;
} IntVec;
 
// Appends x. Returns false (v unchanged) if out of memory.
bool vec_push(IntVec *v, int x) {
    if (v->len == v->cap) {
        size_t cap = v->cap ? v->cap * 2 : 8;
        if (cap > SIZE_MAX / sizeof *v->data) return false;
        int *p = realloc(v->data, cap * sizeof *v->data);
        if (!p) return false;
        v->data = p;
        v->cap = cap;
    }
    v->data[v->len++] = x;
    return true;
}
 
// Bounds-checked read: false if i is out of range.
bool vec_get(const IntVec *v, size_t i, int *out) {
    if (i >= v->len) return false;
    *out = v->data[i];
    return true;
}
 
void vec_free(IntVec *v) {
    free(v->data);
    *v = (IntVec){};          // data = null, len = cap = 0
}
 
int main(void) {
    IntVec v = {};
    for (int i = 0; i < 100; i++)
        if (!vec_push(&v, i * i)) { vec_free(&v); return 1; }
    int x;
    if (vec_get(&v, 99, &x)) printf("%d\n", x);    // 9801
    printf("len %zu cap %zu\n", v.len, v.cap); // 100 128
    vec_free(&v);
}

Pointers to elements (&v.data[3]) become invalid on the next push that reallocates: store indexes instead.

A singly linked list

Insert, delete and free with a pointer to the link being changed (Node **), which removes the special case for the head.

list.c
#include <stdio.h>
#include <stdlib.h>
 
typedef struct Node {
    int value;
    struct Node *next;
} Node;
 
// Inserts at *link; returns false if out of memory.
bool list_push(Node **link, int value) {
    Node *n = malloc(sizeof *n);
    if (!n) return false;
    *n = (Node){.value = value, .next = *link};
    *link = n;
    return true;
}
 
// Inserts keeping the list in ascending order.
bool list_insert_sorted(Node **head, int value) {
    Node **link = head;
    while (*link && (*link)->value < value)
        link = &(*link)->next;
    return list_push(link, value);
}
 
// Removes the first node holding value; true if found.
bool list_remove(Node **head, int value) {
    for (Node **link = head; *link; link = &(*link)->next) {
        if ((*link)->value == value) {
            Node *dead = *link;
            *link = dead->next;       // unlink, then free
            free(dead);
            return true;
        }
    }
    return false;
}
 
void list_free(Node **head) {
    Node *n = *head;
    while (n) {
        Node *next = n->next;         // read before free
        free(n);
        n = next;
    }
    *head = nullptr;
}
 
int main(void) {
    Node *head = nullptr;
    int xs[] = {5, 1, 4, 2, 3};
    for (size_t i = 0; i < 5; i++)
        if (!list_insert_sorted(&head, xs[i])) break;
    list_remove(&head, 4);
    for (Node *n = head; n; n = n->next)
        printf("%d ", n->value);      // 1 2 3 5
    putchar('\n');
    list_free(&head);
}
head …2c0 link &head …308 …328 n …320 1 …300 at …2c0 2 …2e0 …320 at …300 4 4 …2a0 …2a0 at …2e0 5 null at …2a0 3 …2e0 …2a0 at …320 freed heap: one 16-byte malloc block per node (value | next) stack list_insert_sorted(&head, 3); // link = &head list order 1 2 4 5 is not address order: 5 was allocated first while (*link && (*link)->value < 3) link = &(*link)->next; link holds the address of a next field: …308 is node 2 + 8 Node *n = malloc(sizeof *n); *n = (Node){.value = 3, .next = *link}; the new node already points at node 4; the list is unchanged *link = n; one store relinks the list; for the head it would change head dead = *link; *link = dead->next; free(dead); list_remove(&head, 4): unlink node 4 first, then free it

1/5

After inserting 5, 1, 4 and 2 the list is 1 → 2 → 4 → 5. Each node is its own malloc(16) block, 32 bytes apart because malloc adds a header and rounds up, and in allocation order (5 came first), not list order. Inserting 3 starts with link = &head.

A Node ** points at the link to change, so inserting or removing at the head needs no special case.

Read a whole file into memory

For configs, small data files and pipes: one heap buffer grown by doubling. Works for stdin too, unlike tricks based on fseek and ftell.

slurp.c
#include <stdio.h>
#include <stdlib.h>
 
// Reads all of f into a new buffer with a '\0' after the
// data. Returns null on error. Caller frees the result.
char *read_all(FILE *f, size_t *len_out) {
    size_t cap = 4096, len = 0;
    char *buf = malloc(cap);
    if (!buf) return nullptr;
    for (;;) {
        len += fread(buf + len, 1, cap - len - 1, f);
        if (len < cap - 1) break;     // end of file or error
        char *tmp = realloc(buf, cap * 2);
        if (!tmp) { free(buf); return nullptr; }
        buf = tmp;
        cap *= 2;
    }
    if (ferror(f)) { free(buf); return nullptr; }
    buf[len] = '\0';    // handy for text; data may hold 0s
    if (len_out) *len_out = len;
    return buf;
}
 
int main(int argc, char *argv[]) {
    FILE *f = argc > 1 ? fopen(argv[1], "rb") : stdin;
    if (!f) { perror(argv[1]); return 1; }
    size_t len = 0;
    char *data = read_all(f, &len);
    if (f != stdin) fclose(f);
    if (!data) { perror("read"); return 1; }
    printf("%zu bytes\n", len);
    free(data);
}

A 2-D matrix in one allocation

For numeric grids of runtime size: one block, row-major, freed with a single free, and portable to compilers without VLA types.

matrix.c
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
 
typedef struct {
    size_t rows, cols;
    double data[];    // rows * cols values, row after row
} Matrix;
 
// Zero-filled rows x cols matrix, or null. free() it.
Matrix *mat_new(size_t rows, size_t cols) {
    size_t room = SIZE_MAX - sizeof(Matrix);
    room /= sizeof(double);      // most doubles that fit
    if (cols != 0 && rows > room / cols) return nullptr;
    size_t n = rows * cols;
    Matrix *m = calloc(1, sizeof *m + n * sizeof m->data[0]);
    if (!m) return nullptr;
    m->rows = rows;
    m->cols = cols;
    return m;
}
 
static inline double *mat_at(Matrix *m, size_t r,
                             size_t c) {
    return &m->data[r * m->cols + c];
}
 
int main(void) {
    Matrix *a = mat_new(2, 3);
    if (!a) return 1;
    for (size_t r = 0; r < a->rows; r++)
        for (size_t c = 0; c < a->cols; c++)
            *mat_at(a, r, c) = (double)(r * 10 + c);
    printf("%g\n", *mat_at(a, 1, 2));    // 12
    free(a);                             // one free
}

Loop with the column index innermost: consecutive c values are adjacent in memory, which is far faster than striding down columns.

A fixed-size pool with a free list

For many short-lived objects of one type: O(1) allocate and release, no per-object malloc, and the whole pool freed at once.

pool.c
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
 
typedef struct { float x, y, vx, vy; } Particle;
 
typedef union Slot {      // a free slot holds the link,
    union Slot *next;     // a used one holds the object
    Particle item;
} Slot;
 
typedef struct {
    Slot *slots;
    Slot *free_list;
} Pool;
 
bool pool_init(Pool *p, size_t n) {
    *p = (Pool){};
    if (n == 0 || n > SIZE_MAX / sizeof(Slot)) return false;
    p->slots = malloc(n * sizeof *p->slots);
    if (!p->slots) return false;
    for (size_t i = 0; i + 1 < n; i++)    // chain the slots
        p->slots[i].next = &p->slots[i + 1];
    p->slots[n - 1].next = nullptr;
    p->free_list = p->slots;
    return true;
}
 
Particle *pool_alloc(Pool *p) {           // O(1)
    Slot *s = p->free_list;
    if (!s) return nullptr;               // pool exhausted
    p->free_list = s->next;
    return &s->item;
}
 
void pool_release(Pool *p, Particle *item) {   // O(1)
    Slot *s = (Slot *)item;   // a union and its members
    s->next = p->free_list;   // share one address
    p->free_list = s;
}
 
void pool_destroy(Pool *p) {
    free(p->slots);           // every object at once
    *p = (Pool){};
}
 
int main(void) {
    Pool pool;
    if (!pool_init(&pool, 1024)) return 1;
    Particle *a = pool_alloc(&pool);
    Particle *b = pool_alloc(&pool);
    if (!a || !b) return 1;
    *a = (Particle){.x = 1, .vx = 0.5f};
    pool_release(&pool, a);
    Particle *c = pool_alloc(&pool);   // reuses a's slot
    printf("%d\n", c == a);            // 1
    pool_destroy(&pool);
}

Split a string into owned tokens

When a function returns several allocations: a null-terminated array of strings plus one function that frees all of them, the same shape as argv.

split.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
void free_tokens(char **toks) {
    if (!toks) return;
    for (char **t = toks; *t; t++) free(*t);
    free(toks);
}
 
// Splits s at every sep. Returns a null-terminated array of
// new strings (free with free_tokens), or null on failure.
char **split(const char *s, char sep) {
    size_t n = 1;
    for (const char *c = s; *c; c++) n += (*c == sep);
    char **toks = calloc(n + 1, sizeof *toks);  // + null
    if (!toks) return nullptr;
    const char delim[2] = {sep, '\0'};
    for (size_t i = 0; i < n; i++) {
        size_t len = strcspn(s, delim);
        toks[i] = strndup(s, len);           // C23
        if (!toks[i]) { free_tokens(toks); return nullptr; }
        s += len + 1;
    }
    return toks;
}
 
int main(void) {
    char **parts = split("red,green,,blue", ',');
    if (!parts) return 1;
    for (size_t i = 0; parts[i]; i++)
        printf("[%s] ", parts[i]); // [red] [green] [] [blue]
    putchar('\n');
    free_tokens(parts);
}

Find a leak with ASan and Valgrind

When memory use grows or a test suite needs a leak check: build with ASan (LeakSanitizer reports at exit), or run an unmodified binary under Valgrind.

leak.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
// Returns a heap copy of s with a '!' appended.
// The caller owns the result and must free() it.
static char *shout(const char *s) {
    size_t n = strlen(s);
    char *out = malloc(n + 2);
    if (out == nullptr) return nullptr;
    memcpy(out, s, n);
    out[n] = '!';
    out[n + 1] = '\0';
    return out;
}
 
int main(void) {
    const char *words[] = {"hi", "hey", "yo"};
    for (size_t i = 0; i < 3; i++) {
        char *loud = shout(words[i]);
        if (loud) puts(loud);
        // BUG: missing free(loud);
    }
}
gcc -std=c23 -g -fsanitize=address leak.c -o leak && ./leak
gcc -std=c23 -g -O0 leak.c -o leak && \
    valgrind --leak-check=full ./leak
==5812==ERROR: LeakSanitizer: detected memory leaks
 
Direct leak of 13 byte(s) in 3 object(s) allocated from:
    #0 0x7fb721efd9c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
    #1 0x55bfbb0852f8 in shout leak.c:9
    #2 0x55bfbb085530 in main leak.c:20
 
SUMMARY: AddressSanitizer: 13 byte(s) leaked in 3 allocation(s).
 
==5820== HEAP SUMMARY:
==5820==     in use at exit: 13 bytes in 3 blocks
==5820==   total heap usage: 4 allocs, 1 frees, 4,109 bytes allocated
==5820==
==5820== 13 bytes in 3 blocks are definitely lost in loss record 1 of 1
==5820==    at 0x4846828: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==5820==    by 0x1091F8: shout (leak.c:9)
==5820==    by 0x10929E: main (leak.c:20)

Both name the allocation site (shout, line 9) and its caller (main, line 20): the fix is a free(loud); after puts. "Definitely lost" means no pointer to the block remains; "still reachable" blocks are pointed to by globals at exit and are usually harmless. The extra Valgrind allocation is stdout's 4 KiB buffer.

References