Most programmers know realloc exists but don’t know when it copies memory and when it doesn’t. That uncertainty leads to either unnecessary copying or a dangerous pattern where the original pointer is overwritten with NULL before the old memory is freed. Here is what actually happens inside realloc.
What realloc Does
#include <stdlib.h>
void *realloc(void *ptr, size_t new_size);Three outcomes are possible:
- In-place extension: If there is enough free space immediately after the current block,
reallocexpands it in place. No data is moved. The returned pointer is the same asptr. - Move and copy: If there is no space to extend,
reallocallocates a new block, copies the old data, frees the old block, and returns the new pointer. - Failure: Returns NULL if allocation fails. The original block is NOT freed — you still own it.
#include <stdio.h>
#include <stdlib.h>
int main() {
int *arr = malloc(5 * sizeof(int));
/* After realloc, arr may point to the same or a different address */
int *new_arr = realloc(arr, 10 * sizeof(int));
if (new_arr == NULL) {
/* arr is still valid — the original 5-int block was NOT freed */
free(arr);
return 1;
}
/* If realloc moved the data, arr is now a dangling pointer — do not use it */
arr = new_arr; /* always replace arr with the return value */
free(arr);
return 0;
}The Most Common Bug
/* WRONG: overwrites arr before checking for failure */
arr = realloc(arr, new_size);
if (arr == NULL) {
/* arr is NULL — original pointer is lost — memory leaked */
return 1;
}
/* CORRECT: use a temporary */
int *tmp = realloc(arr, new_size);
if (tmp == NULL) {
/* arr still valid — can free it or continue with old size */
free(arr);
return 1;
}
arr = tmp;If you write arr = realloc(arr, new_size) and realloc fails, you lose the original pointer. The memory it pointed to is still allocated — you just cannot free it anymore. Memory leak.
Implementing a Dynamic Array
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int *data;
size_t count;
size_t capacity;
} IntVec;
IntVec *vec_new(void) {
IntVec *v = malloc(sizeof(IntVec));
v->data = malloc(4 * sizeof(int));
v->count = 0;
v->capacity = 4;
return v;
}
int vec_push(IntVec *v, int value) {
if (v->count == v->capacity) {
size_t new_cap = v->capacity * 2; /* double capacity */
int *tmp = realloc(v->data, new_cap * sizeof(int));
if (!tmp) return -1;
v->data = tmp;
v->capacity = new_cap;
}
v->data[v->count++] = value;
return 0;
}
void vec_free(IntVec *v) {
free(v->data);
free(v);
}
int main() {
IntVec *v = vec_new();
for (int i = 0; i < 20; i++) vec_push(v, i);
for (size_t i = 0; i < v->count; i++) printf("%d ", v->data[i]);
printf("n");
vec_free(v);
return 0;
}Why Doubling Is the Right Growth Strategy
Doubling capacity on each overflow means that N insertions require O(log N) reallocations, each realloc copies O(current_size) elements. The total copy work is: N/2 + N/4 + … + 1 = O(N). Amortized over N insertions, each insertion copies O(1) elements. This is why std::vector in C++ (and every serious dynamic array implementation) doubles the capacity.
Growing by a fixed amount (e.g., +10 elements) would require O(N) reallocations for N insertions, each potentially copying O(N) elements — O(N²) total work.
realloc with Size Zero
/* Behavior is implementation-defined in C: */
void *p = realloc(ptr, 0);
/* May return NULL (freeing ptr) or a non-NULL pointer to zero-size allocation */
/* Never rely on this — call free() explicitly *//* realloc(NULL, size) is equivalent to malloc(size) */
int *arr = realloc(NULL, 10 * sizeof(int)); /* valid */realloc(NULL, size) is guaranteed to behave like malloc(size). This can simplify code that initializes a buffer to NULL and reallocs on first use. See our comprehensive guide on malloc, calloc, and realloc for how these three allocation functions differ. The dynamic array pattern above is discussed alongside stack memory in our stack vs heap guide.
TL;DR
reallocmay move data to a new address or extend in place — you cannot predict which- Always use a temporary pointer:
tmp = realloc(p, size); if (tmp) p = tmp; - On failure,
reallocreturns NULL but does NOT free the original block — you still own it - Double capacity on overflow for O(1) amortized push — never grow by fixed amounts
realloc(NULL, size)==malloc(size)— can initialize a growable buffer to NULL and realloc from the start- After a successful realloc, the old pointer may be dangling — do not use it
realloc(ptr, 0)behavior is implementation-defined — callfreeexplicitly instead