malloc(size) requests a block of size bytes from the heap, returning a pointer to it (or NULL if the request fails); free(ptr) releases that block back to the system. calloc additionally zero-initializes the memory it allocates; realloc resizes an existing allocation, possibly moving it to a new address. Unlike languages with automatic memory management, C never reclaims heap memory on its own — every successful malloc/calloc/realloc must eventually be matched by exactly one free, or that memory leaks for the remainder of the program's run.
malloc and free: the basic pair
Always check malloc's return value against NULL before using it — a failed allocation (the system is out of memory) returns NULL, and dereferencing that would crash the program immediately.
int *numbers = malloc(5 * sizeof(int)); // room for 5 ints
if (numbers == NULL) {
fprintf(stderr, "allocation failed\n");
return 1;
}
for (int i = 0; i < 5; i++) {
numbers[i] = i * i;
}
free(numbers); // release the memory — required, nothing does this automatically
numbers = NULL; // good practice: prevents accidentally using the now-invalid pointerTwo real, common bugs: leaks and use-after-free
Neither of these is caught by the compiler — a leak just silently consumes more and more memory over a program's life; a use-after-free can appear to work by coincidence and then crash unpredictably later, since the freed memory might still contain its old data until something else overwrites it.
// LEAK: allocated memory never freed
void leaky(void) {
int *data = malloc(100 * sizeof(int));
// ... use data ...
// missing free(data) — this memory is gone until the program exits
}
// USE-AFTER-FREE: accessing memory after it's been released
int *p = malloc(sizeof(int));
*p = 42;
free(p);
printf("%d\n", *p); // UNDEFINED BEHAVIOR — p no longer owns this memoryrealloc: resizing an existing allocation
realloc may need to move the block to a different address if it can't grow in place — always capture its return value in a new variable rather than overwriting the original pointer directly, since realloc returns NULL on failure without freeing the original block, and overwriting the only reference to it would leak that memory.
int *arr = malloc(5 * sizeof(int));
int *bigger = realloc(arr, 10 * sizeof(int)); // grow to 10 ints
if (bigger == NULL) {
// realloc failed — arr is STILL VALID and must still be freed
free(arr);
return 1;
}
arr = bigger; // safe to reassign now that we know it succeeded