Dynamic Memory Allocation

This section documents C23 (ISO/IEC 9899:2024), per ISO/IEC JTC1/SC22/WG14’s freely available working draft N3220, which WG14 documents as differing from the published standard only editorially — the reference these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against the WG14 draft and cppreference.com’s C reference before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Objects with allocated storage duration live from the call that creates them until the free that destroys them — neither bound tied to any scope. That freedom is why C can build data structures of any shape, and why manual memory management is the language’s most-cited hazard.

The Allocation Functions

Function Returns Notes

malloc(size)

Pointer to size uninitialized bytes

The default. Contents are indeterminate — reading before writing is UB.

calloc(n, size)

Pointer to n * size bytes, all zero

Also checks n * size for overflow, which malloc(n * size) does not.

realloc(p, size)

Pointer to a block of size bytes, contents preserved

May move the block. realloc(nullptr, n) is malloc(n).

aligned_alloc(align, size)

Pointer aligned to align

C11. size must be a multiple of align. Freed with plain free.

free(p)

Nothing

free(nullptr) is explicitly a no-op. Freeing anything else twice is UB.

free_sized(p, size) / free_aligned_sized(p, align, size)

Nothing

C23. You hand the size back, letting the allocator skip its own bookkeeping.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    // 1. malloc: uninitialized. Note "sizeof *numbers", not "sizeof(int)".
    int *numbers = malloc(4 * sizeof *numbers);
    if (numbers == nullptr) {               // ALWAYS check
        return EXIT_FAILURE;
    }
    for (size_t i = 0; i < 4; ++i) {
        numbers[i] = (int)i * 10;           // write before reading
    }

    // 2. calloc: zeroed, and overflow-checked in the multiplication.
    int *zeros = calloc(4, sizeof *zeros);
    if (zeros == nullptr) {
        free(numbers);
        return EXIT_FAILURE;
    }

    // 3. A string copy on the heap.
    const char *source = "hello";
    char *copy = malloc(strlen(source) + 1);        // +1 for the NUL
    if (copy == nullptr) {
        free(numbers);
        free(zeros);
        return EXIT_FAILURE;
    }
    strcpy(copy, source);                           // the size was computed from source

    printf("%d %d %s\n", numbers[3], zeros[0], copy);

    free(copy);
    free(zeros);
    free(numbers);
    return 0;
}

malloc(0) may return either a null pointer or a unique pointer that must still be freed — so a null return is only unambiguously an error when the requested size was nonzero.

The Lifecycle of a Heap Object

flowchart LR A([need an object whose lifetime
outlives its scope]) --> B["p = malloc(n)"] B --> C{"p == nullptr?"} C -->|yes: allocation failed| D[handle the failure:
return an error, free what you hold] C -->|no| E[initialize the bytes
malloc leaves them indeterminate] E --> F[use the object
read and write through p] F --> G{need a different size?} G -->|yes| H["tmp = realloc(p, m)
assign only if tmp != nullptr"] H --> F G -->|no| I["free(p)"] I --> J([p is now indeterminate:
set p = nullptr]) F -.->|forgot to free| K[["memory leak"]] J -.->|used again| L[["use-after-free"]] I -.->|freed twice| M[["double free"]]

Growing Arrays

The single most common dynamic structure in C is a growable array: a pointer, a count and a capacity. realloc is what grows it, and the safe idiom never assigns the result directly to the pointer being reallocated:

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

struct IntVec {
    int *data;
    size_t count;
    size_t capacity;
};

static bool vec_push(struct IntVec *v, int value)
{
    if (v->count == v->capacity) {
        size_t new_capacity = (v->capacity == 0) ? 4 : v->capacity * 2;

        // Guard the multiplication before it reaches realloc.
        if (new_capacity > SIZE_MAX / sizeof *v->data) {
            return false;
        }

        // Assign to a TEMPORARY: if realloc fails it returns nullptr and the
        // original block is still valid -- overwriting v->data would leak it.
        int *grown = realloc(v->data, new_capacity * sizeof *v->data);
        if (grown == nullptr) {
            return false;
        }

        v->data = grown;
        v->capacity = new_capacity;
    }

    v->data[v->count++] = value;
    return true;
}

static void vec_free(struct IntVec *v)
{
    free(v->data);
    v->data = nullptr;              // leave the struct in a safe, reusable state
    v->count = 0;
    v->capacity = 0;
}

int main(void)
{
    struct IntVec v = { };          // C23 empty initializer: all members zero

    for (int i = 0; i < 10; ++i) {
        if (!vec_push(&v, i * i)) {
            vec_free(&v);
            return EXIT_FAILURE;
        }
    }

    printf("count=%zu capacity=%zu last=%d\n", v.count, v.capacity, v.data[v.count - 1]);
    vec_free(&v);
    return 0;
}

Doubling the capacity is what keeps n pushes amortized O(n); growing by a constant makes it O(n²).

Ownership and Consistency Rules

C has no destructors and no borrow checker, so ownership is a convention you document. The rules that make it workable:

  • Every allocation has exactly one owner — the code responsible for freeing it. Say so in the header: "returns a buffer the caller must `free`".

  • Free in the reverse order of acquisition, and use one exit path. The goto-to-cleanup pattern from Control Flow exists for exactly this.

  • Match the allocator to the deallocator: free for everything from malloc/calloc/realloc/ aligned_alloc; never free a pointer you did not get from one of them, and never free the interior of a block.

  • Pair every constructor with a destructor — thing_create/thing_destroy — and have the destructor accept nullptr the way free does.

  • Set the pointer to nullptr after freeing. It converts a use-after-free (undefined) into a null dereference (a reliable crash).

  • Prefer the stack. An object that fits in a scope should live in that scope: no allocation, no failure path, no leak.

#include <stdlib.h>
#include <string.h>

struct Widget {
    char *name;              // owned by this Widget
    int *values;             // owned by this Widget
    size_t value_count;
};

// Constructor: either fully succeeds, or allocates nothing and returns nullptr.
static struct Widget *widget_create(const char *name, size_t value_count)
{
    struct Widget *w = calloc(1, sizeof *w);            // zeroed: every pointer is null
    if (w == nullptr) {
        return nullptr;
    }

    w->name = malloc(strlen(name) + 1);
    if (w->name == nullptr) {
        goto fail;
    }
    strcpy(w->name, name);

    w->values = calloc(value_count, sizeof *w->values);
    if (w->values == nullptr) {
        goto fail;
    }
    w->value_count = value_count;

    return w;

fail:
    free(w->name);           // free(nullptr) is a no-op, so this is safe either way
    free(w);
    return nullptr;
}

// Destructor: accepts nullptr, frees members before the struct itself.
static void widget_destroy(struct Widget *w)
{
    if (w == nullptr) {
        return;
    }
    free(w->values);
    free(w->name);
    free(w);
}

int main(void)
{
    struct Widget *w = widget_create("gauge", 16);
    if (w == nullptr) {
        return EXIT_FAILURE;
    }

    w->values[0] = 42;
    widget_destroy(w);
    widget_destroy(nullptr);        // harmless
    return 0;
}

Allocating a Flexible Array Member

One allocation for a header plus its payload — fewer allocations, one free, and the data is contiguous:

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

struct Matrix {
    size_t rows;
    size_t cols;
    double cells[];             // flexible array member: not counted by sizeof
};

static struct Matrix *matrix_create(size_t rows, size_t cols)
{
    if (rows != 0 && cols > SIZE_MAX / rows) {
        return nullptr;                                     // overflow guard
    }
    size_t cell_count = rows * cols;

    if (cell_count > (SIZE_MAX - sizeof(struct Matrix)) / sizeof(double)) {
        return nullptr;
    }

    struct Matrix *m = malloc(sizeof *m + cell_count * sizeof *m->cells);
    if (m == nullptr) {
        return nullptr;
    }

    m->rows = rows;
    m->cols = cols;
    for (size_t i = 0; i < cell_count; ++i) {
        m->cells[i] = 0.0;
    }
    return m;
}

static double *matrix_at(struct Matrix *m, size_t r, size_t c)
{
    return &m->cells[r * m->cols + c];      // row-major, computed by hand
}

int main(void)
{
    struct Matrix *m = matrix_create(3, 4);
    if (m == nullptr) {
        return EXIT_FAILURE;
    }

    *matrix_at(m, 2, 3) = 1.5;
    printf("%zux%zu, cell(2,3) = %g\n", m->rows, m->cols, *matrix_at(m, 2, 3));

    free(m);                    // one free for header and payload together
    return 0;
}

Note the overflow guards. malloc(rows * cols * sizeof(double)) with attacker-controlled dimensions is a classic heap overflow: the multiplication wraps, a small block is allocated, and the writes run past it.

C23: Sized Deallocation

#include <stdlib.h>

int main(void)
{
    size_t n = 64;

    void *p = malloc(n);
    if (p == nullptr) {
        return EXIT_FAILURE;
    }

    free_sized(p, n);           // C23: the size must match the request exactly

    void *q = aligned_alloc(64, 128);
    if (q == nullptr) {
        return EXIT_FAILURE;
    }
    free_aligned_sized(q, 64, 128);

    return 0;
}

free_sized and free_aligned_sized are new in C23 and need a recent libc (glibc 2.42 or later); plain free remains correct for every allocation, so treat these as an optimization to adopt once your baseline supports them. The example above is written to the standard but is not compile-verified here, because the glibc in this environment predates it.

The Classic Defects

Defect What happens How it is caught

Memory leak

The block is never freed; the process grows.

-fsanitize=leak (LeakSanitizer, included in ASan), valgrind --leak-check=full.

Use-after-free

Reading or writing through a freed pointer. Silent corruption, or a security hole.

-fsanitize=address, Valgrind. Setting the pointer to nullptr prevents it.

Double free

free called twice on the same block; corrupts allocator metadata.

-fsanitize=address; glibc also aborts with "double free or corruption".

Buffer overflow

Writing past the end of a block — usually an off-by-one or an unchecked size.

-fsanitize=address, -D_FORTIFY_SOURCE=3, valgrind.

Uninitialized read

Using `malloc’d bytes before writing them.

-fsanitize=memory (MSan, Clang), valgrind. Or use calloc.

Unchecked allocation

Dereferencing a null return under memory pressure.

-Wanalyzer-possible-null-dereference (GCC), clang --analyze.

Run the test suite under sanitizers as a matter of course — they find these at the moment they happen rather than at the crash three functions later:

$ clang -std=c23 -Wall -Wextra -g -fsanitize=address,undefined -o app app.c
$ ./app
==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x502000000010
    #0 0x... in main app.c:14

$ valgrind --leak-check=full --show-leak-kinds=all ./app
==12346== HEAP SUMMARY:
==12346==     definitely lost: 40 bytes in 1 blocks
==12346==     indirectly lost: 16 bytes in 1 blocks
==12346==       possibly lost: 0 bytes in 0 blocks
==12346==     still reachable: 72 bytes in 3 blocks

ASan and Valgrind cannot both instrument the same run — pick one — and neither replaces the other entirely: ASan is far faster and catches stack/global overflows, Valgrind needs no rebuild.

See Also