Dynamic memory patterns and allocation bugs
Ownership rules, growing buffers, flexible array members, and how to find leaks, double frees and off-by-one writes with valgrind.
Ownership and growing buffers
C has no destructors, so ownership is a convention you write down. Pick one rule per pointer and stick to it: the function that allocated it frees it, or the caller does. Document it in the header comment.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char *data;
size_t len; /* bytes used, excluding NUL */
size_t cap; /* bytes allocated */
} Buf;
int buf_init(Buf *b, size_t cap) {
b->data = malloc(cap);
if (!b->data) return -1;
b->len = 0;
b->cap = cap;
b->data[0] = '\0';
return 0;
}
int buf_append(Buf *b, const char *s) {
size_t add = strlen(s);
if (b->len + add + 1 > b->cap) { /* need to grow */
size_t nc = b->cap ? b->cap : 16;
while (nc < b->len + add + 1) nc *= 2; /* keep +1 for the terminator */
char *p = realloc(b->data, nc);
if (!p) return -1; /* original block still valid */
b->data = p;
b->cap = nc;
}
memcpy(b->data + b->len, s, add + 1);
b->len += add;
return 0;
}
void buf_free(Buf *b) { free(b->data); b->data = NULL; b->len = b->cap = 0; }- Assign
reallocto a temporary. Writingb->data = realloc(b->data, n)leaks the old block when the call fails. - Doubling the capacity amortises appends to constant time; growing by a fixed small step makes repeated appends quadratic.
calloc(n, size)zeroes the memory and checks the multiplication for overflow.mallocleaves it indeterminate.- After
free, set the pointer toNULL. A null free is harmless; a stale non-null pointer is a use-after-free waiting to happen.
Flexible array members
Since C99 a struct can end with an unsized array, letting you allocate the header and its payload in one block. This is how a single free cleans up everything.
typedef struct {
size_t len;
char text[]; /* flexible array member: must be last */
} Str;
Str *str_new(const char *s) {
size_t n = strlen(s) + 1;
Str *p = malloc(sizeof *p + n); /* one allocation for header + data */
if (!p) return NULL;
p->len = n - 1;
memcpy(p->text, s, n);
return p;
}
/* free(p) is now sufficient */A struct with a flexible array member cannot be a member of another struct or an array element, and sizeof reports only the fixed part.
The four bugs valgrind finds
| Bug | Symptom | Detector |
|---|---|---|
| Leak | Memory grows over time; process RSS climbs | valgrind --leak-check=full |
| Use after free | Garbage data, or a crash much later | AddressSanitizer, valgrind invalid read/write |
| Double free | Abort inside the allocator | AddressSanitizer, glibc heap corruption messages |
| Off-by-one write | Silently corrupts the next object or the heap metadata | -fsanitize=address reports the exact byte |
# sanitizers first: fast and precise
cc -std=c17 -g -O1 -fsanitize=address,undefined -fno-omit-frame-pointer app.c -o app
./app
# valgrind when you cannot recompile, or want leak accounting
valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./app
valgrind --tool=helgrind ./threaded_app # race detection💡
malloc(0) may return NULL or a unique pointer; both are valid, and you must not dereference it. Every malloc result needs a NULL check, because malloc(sizeof(int)) failing is not hypothetical under memory pressure.FAQ
Why did my program work then crash later?
Writing past the end of a buffer usually corrupts adjacent memory that is only read much later, or corrupts the allocator's own metadata. That delayed failure is the signature of a memory bug; enable the sanitisers and the crash moves to the exact line.
Does realloc preserve my data?
Yes, it copies the existing contents. Newly added bytes are uninitialised. If it returns NULL the old block is untouched and you must free it yourself.
Related
Pointers and memory Debugging with gdb, sanitizers and valgrind
Last refreshed 2026-09-18.