Polyglot
Languages C memory
C § memory

Memory

C requires the programmer to manage memory directly. There is no garbage collector, no reference counting in the runtime, and no implicit copying of long-lived objects. The language exposes the underlying memory layout through pointers and through a small library — malloc, calloc, realloc, free — and leaves the questions of when memory is allocated, who owns it, and when it is released to the program. The discipline of correct memory management is the chief practical concern of C programming, and the source of a substantial fraction of the bugs and security vulnerabilities found in C software.

Storage durations

The standard recognises four storage durations, each with distinct lifetime rules.

Automatic storage

Variables declared at block scope without static have automatic duration: storage is reserved on entry to the block and released on exit. The conventional implementation places these objects on the stack: a region of memory used as a last-in-first-out structure for the activation records of nested function calls.

void f(int n) {
    int  x = 42;        /* automatic */
    int  arr[100];      /* automatic */
    /* x and arr exist for the duration of this call */
}

Allocation is essentially free — adjusting the stack pointer is one instruction — and deallocation is automatic at scope exit. The trade-off is lifetime: a pointer to an automatic object becomes invalid the moment the enclosing block ends, even if the pointer is returned or stored elsewhere.

Static storage

Variables declared at file scope, and variables declared at block scope with static, have static duration: storage is reserved at program startup and released at program termination. Their initial values are set at startup and are zero by default; explicit initialisers are evaluated at compile time.

static int call_count = 0;       /* file-scope; initialised once */

void f(void) {
    static int local_count = 0;  /* block-scope, static-duration */
    ++call_count;
    ++local_count;
}

Static-duration objects live for the entire program. They are placed in a fixed region of memory — historically called the BSS (for zero-initialised) or data (for non-zero-initialised) segment — and remain valid for any pointer that has been taken to them.

Allocated storage

Memory obtained from the standard allocator has allocated duration: it exists from a successful call to malloc, calloc, aligned_alloc, or realloc until a corresponding free. The allocator manages a region of memory called the heap, structurally separate from the stack.

int *p = malloc(100 * sizeof *p);   /* allocated */
if (!p) { /* allocation failed */ }
/* … use p … */
free(p);

Allocated storage requires the program to track the lifetime: the system does not reclaim memory whose pointers go out of scope. Failing to free is a leak; freeing twice or freeing memory not returned by an allocator is undefined behaviour.

Thread storage

Variables declared with _Thread_local (C11) or thread_local (C23) have thread storage duration: one instance exists per thread, with the same start-to-end-of-thread lifetime. The implementation model is platform-specific (TLS slots on most platforms); the language guarantee is that the variable is private to the executing thread.

_Thread_local int last_error = 0;

Thread-local objects are most useful for per-thread caches and error states. They are not portable to freestanding implementations and may have non-trivial setup costs on some platforms.

The stack and the heap

The conventional implementation distinguishes the stack from the heap:

  • The stack holds activation records: each function call pushes a frame containing local variables, parameters, the return address, and saved registers. The stack grows in one direction (downward on most architectures) and shrinks as functions return. Allocation is implicit and free; the size is fixed by the operating system, typically 1–8 MiB per thread.

  • The heap holds memory obtained from malloc and friends. The allocator maintains internal data structures (free lists, chunk headers, sometimes per-thread caches) and serves requests of arbitrary sizes from a region the OS provides via system calls (brk, sbrk, mmap on Unix; VirtualAlloc on Windows).

The standard does not require a stack/heap split — a freestanding implementation is at liberty to organise memory differently — but every hosted implementation in commercial production uses the structure.

The chief practical consequences:

  • Automatic objects that are large or whose size is determined at runtime risk stack overflow if the cumulative frame size exceeds the OS limit. Variable-length arrays (C99) are particularly risky; long-running recursion or deep call chains can also trigger it.
  • Heap allocations carry overhead: the allocator needs to track the size of each block (typically 8–16 bytes of header per allocation on 64-bit systems) and may fragment over time.
  • The OS-level memory of a long-running program is mostly heap; valgrind, tcmalloc statistics, and OS tools like /proc/[pid]/status report on it.

The malloc family

The four allocators in <stdlib.h>:

FunctionEffect
malloc(size)Returns a pointer to size bytes of uninitialised memory, or NULL on failure.
calloc(n, size)Returns a pointer to n * size bytes of zero-initialised memory, or NULL on failure.
realloc(p, size)Resizes the allocation p to size bytes; may move the allocation. Returns the new pointer (which may be p) or NULL on failure (the old pointer remains valid).
aligned_alloc(align, size)(C11) Returns a pointer to size bytes aligned to align, or NULL on failure.

A successful allocation returns memory aligned suitably for any standard type. The contents are uninitialised after malloc, zero-initialised after calloc, and mixed-status after realloc (the old contents are preserved up to the smaller of the two sizes; the rest is uninitialised).

The conventional usage:

int *arr = malloc(n * sizeof *arr);
if (!arr) return -1;
/* … use arr … */
free(arr);

Two patterns recur:

  1. Multiplicand-first sizing: malloc(n * sizeof *arr) rather than malloc(sizeof(int) * n). The form with *arr adapts automatically if the type of arr changes; the form with the explicit type does not.
  2. Realloc to a temporary first: realloc returns NULL on failure but leaves the old allocation intact. Assigning the result directly to the variable being resized destroys the old pointer if the call fails, leaking the original allocation. The defence is void *tmp = realloc(p, n); if (!tmp) { /* handle */ } else { p = tmp; }.

free

The free function releases the memory obtained from a previous successful allocator call. After free(p), the value of p is indeterminate: it cannot be dereferenced, compared, or otherwise used. Common discipline is to assign NULL after free, both to make the invalid pointer detectable and to make subsequent free calls (on the now-null pointer) safe — free(NULL) is well-defined and does nothing.

free(p);
p = NULL;

free is a single point of responsibility: every successful allocation must be freed exactly once, by exactly one part of the program. Designing the lifetime — who allocates, who frees, in what order — is the principal task of memory management.

Allocation failure

malloc and its companions return NULL when allocation fails. The standard requires a check; in practice, on systems with overcommit (Linux is the most common), the check often succeeds and the failure surfaces only on first access, when the kernel’s out-of-memory killer terminates the process. The standard nevertheless requires the check, and a portable program performs it.

int *arr = malloc(n * sizeof *arr);
if (!arr) {
    fprintf(stderr, "out of memory\n");
    return EXIT_FAILURE;
}

In long-running programs, allocation-failure handling matters even on overcommit systems: subprocesses, sandboxes, and resource-limited contexts can still produce real NULL returns.

Memory layout and alignment

Every object has a size, given by sizeof, and an alignment requirement: an address constraint that the standard specifies for each type. On most 64-bit platforms, int aligns to 4, double to 8, and pointers to 8.

Within a structure, the implementation may insert padding bytes to satisfy alignment:

struct example {
    char  c1;       /*  1 byte */
                    /*  3 bytes of padding */
    int   x;        /*  4 bytes */
    char  c2;       /*  1 byte */
                    /*  3 bytes of trailing padding */
};
/* sizeof(struct example) == 12 on a typical platform */

Reordering members from largest to smallest minimises padding:

struct better {
    int   x;        /* 4 bytes */
    char  c1;       /* 1 byte */
    char  c2;       /* 1 byte */
                    /* 2 bytes of trailing padding */
};
/* sizeof(struct better) == 8 */

C11 introduced _Alignof to query alignment and _Alignas to specify an over-aligned object. The convenience macros alignof and alignas come from <stdalign.h> (and were promoted to keywords in C23).

#include <stdalign.h>

alignas(64) char cache_line[64];   /* aligned to a typical cache-line boundary */
size_t a = alignof(double);        /* the alignment of double, in bytes */

Allocator patterns

Beyond raw malloc/free, several patterns appear regularly in production C.

Object lifetimes tied to a transaction

A program that processes one request at a time can amortise allocation costs by maintaining a region (also called an arena or bump allocator): a single large allocation from which smaller allocations are carved by advancing a pointer. The region is freed at once when the transaction ends.

typedef struct {
    char  *base;
    size_t size;
    size_t used;
} arena;

void *arena_alloc(arena *a, size_t n) {
    if (a->used + n > a->size) return NULL;
    void *p = a->base + a->used;
    a->used += n;
    return p;
}

The trade-off: allocation is a pointer increment, but no individual deallocation is supported.

Pools for fixed-size allocations

When many objects of the same size are allocated and freed (typical in linked structures, parser nodes, network buffers), a pool allocator maintains a free list of available slots. Allocation pops from the head; deallocation pushes back.

typedef struct slot { struct slot *next; } slot;

static slot *free_list = NULL;

void *pool_alloc(void) {
    if (free_list) { slot *s = free_list; free_list = s->next; return s; }
    return malloc(sizeof(slot));
}

void pool_free(void *p) {
    slot *s = p;
    s->next = free_list;
    free_list = s;
}

Reference-counted shared ownership

When more than one part of a program needs to hold an object, manual reference counting is the conventional pattern:

typedef struct {
    int  refcount;
    /* … data … */
} shared;

shared *shared_acquire(shared *s) { ++s->refcount; return s; }
void    shared_release(shared *s) { if (--s->refcount == 0) free(s); }

Reference counting is straightforward when the ownership graph has no cycles; cycles produce leaks, and the conventional solutions involve weak references or external acyclicity guarantees.

Common defects

The recurring memory-related defects, in approximate order of frequency:

DefectDescription
LeakAn allocation is never freed. The program’s memory grows over time.
Use after freeMemory is read or written after free. Undefined behaviour.
Double freeMemory is freed twice. Undefined behaviour.
Buffer overflowA write extends past the allocated size. Undefined behaviour; a classic security flaw.
Buffer underflowA read or write before the allocated start. Undefined behaviour.
Mismatched allocatorMemory obtained from one allocator is freed via another. Undefined.
Uninitialised readMemory from malloc (or an uninitialised automatic variable) is read before being written. Undefined.
Stack overflowThe cumulative size of activation records exceeds the OS limit. Crash.
Pointer to expired automaticA pointer is taken to an automatic object whose lifetime has ended. Use is undefined.

The contemporary toolchain provides substantial assistance: -fsanitize=address instruments allocations and frees to detect most use-after-free, double-free, and buffer-overflow conditions; -fsanitize=memory (Clang only) detects uninitialised reads; valgrind’s memcheck covers a similar range without recompilation. None of these is a substitute for designing memory ownership clearly, but they catch a substantial fraction of the bugs that remain after careful design.