Growing Buffers: The realloc Idiom
intermediate15 min readLesson 94 of 148
Why temp = realloc(p, n) is the only safe shape, amortized doubling, and shrinking correctly.
The trap realloc sets
realloc(p, new_size) may: extend in place, move the block (copying contents,
freeing the old), or fail ā returning NULL and leaving the original block
valid. That last case is the trap:
p = realloc(p, n * 2); /* BUG: on failure the old block leaks ā
p no longer points at it */
The safe idiom keeps the old pointer until success is confirmed:
void *tmp = realloc(p, n * 2);
if (!tmp) {
/* p is still valid: recover, retry with smaller growth, or fail cleanly */
return 0;
}
p = tmp;
The standard growth loop
typedef struct {
int *data;
size_t len, cap;
} Vec;
int vec_push(Vec *v, int value) {
if (v->len == v->cap) {
size_t ncap = v->cap ? v->cap * 2 : 8; /* amortized doubling */
if (ncap < v->cap) return 0; /* overflow guard */
int *nd = realloc(v->data, ncap * sizeof *nd);
if (!nd) return 0;
v->data = nd;
v->cap = ncap;
}
v->data[v->len++] = value;
return 1;
}
Amortized doubling is why dynamic arrays are "O(1) push": most pushes write one slot; every doubling copies, but copies become geometrically rarer. Total copy work across n pushes is O(n).
Shrinking and emptying
/* shrink: realloc to a smaller size cannot fail in practice, but the
idiom does not hurt */
void *tmp = realloc(v->data, v->len * sizeof *v->data);
if (tmp) v->data = tmp;
v->cap = v->len;
/* the capacity invariant: len <= cap, always */
free a NULL member is fine (free(v->data) when data == NULL is a no-op),
so a "destroy" function needs no special empty case:
void vec_destroy(Vec *v) {
free(v->data); /* free(NULL) is fine */
v->data = NULL;
v->len = v->cap = 0;
}
Check your understanding
- Why does the naive
p = realloc(p, n)leak? (On failure, the old block's only pointer is overwritten.) - Why double rather than grow by a fixed 10? (Fixed growth makes n pushes O(n²) copies; doubling is O(n).)