C / DYNAMIC MEMORY
realloc for growing buffers without losing data
Grow a heap buffer with realloc while keeping every byte already stored, handling failure without leaking and without leaving dangling pointers.
What you will learn
- Assign realloc's result to a temporary so a NULL return does not leak the old block.
- Treat the old pointer and any interior pointers as dead after a successful realloc.
- Double capacity instead of growing by one to keep appends amortized O(1).
- Track length and capacity separately; grown bytes are uninitialized, not zeroed.
Understanding realloc for growing buffers without losing data
realloc(p, n) takes a block you got from malloc, calloc or an earlier realloc and hands back a block of n bytes whose first min(old, n) bytes hold exactly the values that were there before. It is free to satisfy the request by widening the existing block in place, or by allocating a different one, copying your bytes into it and deallocating the original, and it does not tell you which happened. That is precisely why it returns a pointer instead of returning void: realloc answers the question "where does my data live now", and the answer is only good until you ask again.
That return value drives the whole discipline. Writing p = realloc(p, n) bets the block on the call succeeding, because on failure realloc returns NULL and leaves the old block allocated and unchanged, so overwriting p throws away the only handle to data that still exists. Assign into a temporary, check it, and only then commit to p. Once you do commit, the old address is dead in the other direction too: the standard deallocates the old object, so a saved copy of the pointer, a cursor like char *w = buf + used, or a stashed &arr[i] are all dangling, which is why you carry offsets across a growth rather than pointers.
How much you grow by is a correctness-adjacent performance decision. Since each call may have to copy everything stored so far, growing by one element per append costs on the order of n*n/2 element copies to fill n elements, while multiplying capacity by a constant factor turns those copies into a geometric series bounded by a small multiple of n, making appends amortized O(1). Because realloc counts bytes while you count elements, refuse to grow when newcap > SIZE_MAX / sizeof *p, and remember that the bytes above the old size are raw rather than zeroed, so a freshly grown char buffer has no terminator waiting in it.
<stdio.h>
<stdlib.h>
int main(void)
{
int *data = NULL; /* realloc(NULL, n) behaves as malloc(n) */
size_t len = 0, cap = 0;
for (int i = 0; i < 9; i++) {
if (len == cap) {
size_t newcap = cap ? cap * 2 : 2;
int *tmp = realloc(data, newcap * sizeof *data);
if (tmp == NULL) {
free(data); /* old block is still valid and still ours */
fputs("out of memory\n", stderr);
return 1;
}
data = tmp; /* commit only after the check */
cap = newcap;
printf("grew to cap=%2zu while holding %zu values\n", cap, len);
}
data[len++] = i * i;
}
printf("len=%zu cap=%zu:", len, cap);
for (size_t i = 0; i < len; i++)
printf(" %d", data[i]);
putchar('\n');
free(data);
return 0;
}
realloc preserves your bytes but not necessarily your address, so growth means committing a new pointer and rebuilding anything that referred to the old block.
Worked examples
Interior pointers do not survive the move
Shows why a pointer into the buffer must be rebuilt from a stored offset after realloc, even though the characters themselves are preserved.
<stdio.h>
<stdlib.h>
<string.h>
int main(void)
{
char *buf = malloc(8);
if (buf == NULL) return 1;
strcpy(buf, "abcdef");
char *tail = buf + 3; /* a pointer into the block */
size_t off = (size_t)(tail - buf); /* the same spot, as an offset */
char *tmp = realloc(buf, 64);
if (tmp == NULL) { free(buf); return 1; }
buf = tmp;
tail = buf + off; /* rebuilt; the old value is dead */
strcpy(buf + 6, "ghij");
printf("buf = %s\n", buf);
printf("tail = %s\n", tail);
printf("off = %zu\n", off);
free(buf);
return 0;
}
Example explained
Line 1char *tmp = realloc(buf, 64); uses a separate variable, so a NULL return leaves buf pointing at the intact 8-byte block that still has to be freed.
Line 2The first value of tail is unusable after the call: realloc may have deallocated the old object, so tail could hold an address in freed memory even though the letters d, e, f were copied faithfully.
Line 3tail = buf + off; reconstructs the same logical position inside whatever block realloc returned, which is why the offset and not the pointer is the durable thing to store.
Line 4strcpy(buf + 6, "ghij") only fits because the block is now 64 bytes, and it lands directly after the six characters realloc carried over, giving abcdefghij.
Appending strings with a doubling policy, then shrinking to fit
Builds a string with repeated appends, reallocating only when the capacity is exhausted, and trims the block to the exact length at the end.
<stdio.h>
<stdlib.h>
<string.h>
static char *append(char *s, size_t *len, size_t *cap, const char *add)
{
size_t addlen = strlen(add);
size_t need = *len + addlen + 1; /* + 1 for the terminator */
if (need > *cap) {
size_t newcap = *cap ? *cap : 8;
while (newcap < need) newcap *= 2;
char *tmp = realloc(s, newcap);
if (tmp == NULL) return NULL; /* caller still owns the old s */
s = tmp;
*cap = newcap;
}
memcpy(s + *len, add, addlen + 1);
*len += addlen;
return s;
}
int main(void)
{
char *s = NULL;
size_t len = 0, cap = 0;
const char *parts[] = { "dynamic ", "arrays ", "grow ", "geometrically" };
for (size_t i = 0; i < 4; i++) {
char *tmp = append(s, &len, &cap, parts[i]);
if (tmp == NULL) { free(s); return 1; }
s = tmp;
printf("cap=%2zu len=%2zu [%s]\n", cap, len, s);
}
size_t oldcap = cap;
char *fit = realloc(s, len + 1); /* shrink to exactly what is used */
if (fit != NULL) { s = fit; cap = len + 1; }
printf("shrunk cap=%zu -> %zu, text intact: [%s]\n", oldcap, cap, s);
free(s);
return 0;
}
Example explained
Line 1while (newcap < need) newcap *= 2; keeps the doubling policy even when a single append is larger than the remaining gap, so one call always produces enough room.
Line 2Between the first and second output lines no realloc happened at all: len went from 8 to 15 inside the same 16-byte block, which is what tracking capacity separately from length buys you.
Line 3append returns NULL without touching the caller's s, so main can still free the old, complete string instead of leaking it.
Line 4realloc(s, len + 1) shrinks the block and is still assigned through a check, because a shrink is not required to return the same address and is not required to succeed.
Important notes
realloc(NULL, n) is defined to behave exactly like malloc(n), which is why a growth loop can start from a NULL pointer with capacity 0 and needs no special case for the first allocation.
realloc(p, 0) is not a portable way to free: it was implementation-defined in C17 and is undefined behaviour in C23, so call free(p) instead.
Common mistakes
Writing buf = realloc(buf, n); and then checking if (buf == NULL): when the call fails the old block is still allocated but its only pointer has been overwritten, so the data is unreachable and permanently leaked.
Calling realloc once per appended element inside a read loop: correct but quadratic, since each call may copy the whole buffer, so it looks instant at a thousand elements and stalls at ten million.
Assuming a grown buffer is zeroed and calling strlen or strcat on it right after realloc: the bytes past the old size are indeterminate, so the length is garbage and the write can run off the end of the block.
Try it yourself
Change, predict, then run
Starting from a NULL pointer and capacity 0, append the squares of 1 through 50 to an int buffer, doubling capacity only when it is full and printing the capacity each time it changes. Then realloc down to exactly 50 elements and print the first and last values to confirm nothing was lost.
Open the C workspaceCheck your understanding
A program has char *p = malloc(16) holding a string, plus char *end = p + strlen(p). It then does char *tmp = realloc(p, 64); if (tmp) p = tmp; and afterwards writes through end. Why is that a bug even on runs where it appears to work?
- Because realloc leaves the copied bytes indeterminate, so end no longer points at a terminator.
- Because any library call that touches the heap invalidates every result of pointer arithmetic.
- Because a successful realloc ends the old object's lifetime, so end may point into freed memory; only pointers derived from the returned pointer are valid.
- Because realloc preserves the old contents only when it can extend the block in place.
Show answer
On success realloc deallocates the old object, so every pointer derived from the old address, including an interior one like end, is dangling and must be rebuilt as p + offset. Option 3 is the tempting one, but realloc always preserves the leading min(old, new) bytes, copying them when it cannot extend in place; that copying is exactly what can change the address, and when the allocator happens to extend in place instead, the stale end keeps working and the bug hides until the block does move.