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 |
|---|---|---|
|
Pointer to |
The default. Contents are indeterminate — reading before writing is UB. |
|
Pointer to |
Also checks |
|
Pointer to a block of |
May move the block. |
|
Pointer aligned to |
C11. |
|
Nothing |
|
|
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
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:
freefor everything frommalloc/calloc/realloc/aligned_alloc; neverfreea 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 acceptnullptrthe wayfreedoes. -
Set the pointer to
nullptrafter 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;
}
|
|
The Classic Defects
| Defect | What happens | How it is caught |
|---|---|---|
Memory leak |
The block is never freed; the process grows. |
|
Use-after-free |
Reading or writing through a freed pointer. Silent corruption, or a security hole. |
|
Double free |
|
|
Buffer overflow |
Writing past the end of a block — usually an off-by-one or an unchecked size. |
|
Uninitialized read |
Using `malloc’d bytes before writing them. |
|
Unchecked allocation |
Dereferencing a null return under memory pressure. |
|
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
-
Storage Duration, Scope and Linkage — allocated storage among the other three durations.
-
Structures, Unions and Type Aliases — flexible array members and struct layout.
-
Error Handling and Program Failure — cleanup discipline and reporting allocation failure.
-
Build and Tooling — sanitizers, Valgrind and static analysis, including all four leak categories and how to wire it into CI.
-
C++: Memory Management and Smart Pointers — C++ wraps this in RAII,
new/delete, andunique_ptr/shared_ptr.
References
-
WG14 N3220 — the C23 working draft (§6.2.4 "Storage durations of objects", §7.24.3 "Memory management functions").