C lesson 7 of 8
Dynamic Memory in C: malloc and free
How to ask for memory while a program is running, why every malloc must be checked against NULL and matched by exactly one free, and what leaks, dangling pointers and double frees actually are.
Published · Every example on this page was run before it was published.
Every array so far has had its size fixed when the program was written. That is fine for a fixed-size buffer and useless for a list whose length is only known once the program is running. C's answer is to let you ask the system for a block of memory at run time, use it for as long as you like, and give it back when you are finished. The asking and the giving back are both entirely manual, and the bugs that come from getting them wrong have names you should learn before you write the code.
Two Kinds of Storage
A local variable lives on the stack. Its lifetime is the block it was declared in: the memory is reserved automatically when execution reaches the declaration, and released automatically when the block ends. You never think about it, and you cannot keep it past the end of its function.
malloc allocates from the heap instead. A heap block's lifetime has nothing to do with any block of
code: it exists from the moment you allocate it until the moment you free it, even if the function that
created it returned long ago. That is the power, and the entire cost is that nobody frees it for you.
malloc, and Always Checking the Result
malloc comes from <stdlib.h>. It takes a number of bytes and returns a pointer to that much
memory, or NULL if it could not oblige.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *numbers = malloc(5 * sizeof(int));
if (numbers == NULL) {
printf("allocation failed\n");
return 1;
}
for (int i = 0; i < 5; i++) {
numbers[i] = (i + 1) * 100;
}
for (int i = 0; i < 5; i++) {
printf("numbers[%d] = %d\n", i, numbers[i]);
}
free(numbers);
numbers = NULL;
printf("released\n");
return 0;
}numbers[0] = 100
numbers[1] = 200
numbers[2] = 300
numbers[3] = 400
numbers[4] = 500
releasedFive things happen there, and all five are the standard pattern.
The size is computed, not guessed. 5 * sizeof(int) asks for room for five ints whatever an int
costs on this platform. Writing 20 would work here and break on a machine where int is a different
size.
The result is checked against NULL before it is used. Not after; before. This is not defensive
padding you can skip in small programs - it is the only thing standing between a failed allocation and a
dereference of a null pointer. And in this site's WebAssembly sandbox, that dereference does not
reliably crash, so a missing check produces quiet nonsense rather than an obvious failure.
The block is used exactly like an array. numbers[i] works on a pointer because, as the pointers
lesson showed, p[i] means *(p + i). There is no difference in how you read and write it.
free gives the block back, once, when it is no longer needed. free takes the pointer that
malloc returned - not a pointer into the middle of the block, and not a pointer to something that was
never allocated.
The pointer is set to NULL afterwards. free releases the memory but cannot change your copy of
the address, so without that line numbers would still hold an address that is no longer yours.
free(NULL) is explicitly defined to do nothing, which is convenient: a cleanup path does not have to
test before freeing.
malloc Does Not Initialise; calloc Does
The bytes malloc hands you have indeterminate contents. Reading them before you have written something
is undefined behaviour, which is why no example on this page prints a freshly allocated block - there is
no correct output to show, and a run that appeared to show zeroes would be telling you something the
language does not promise.
calloc takes a count and an element size and guarantees the whole block is zeroed, which is a real
guarantee you can rely on:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *tally = calloc(4, sizeof(int));
if (tally == NULL) {
printf("allocation failed\n");
return 1;
}
for (int i = 0; i < 4; i++) {
printf("tally[%d] = %d\n", i, tally[i]);
}
tally[2] = 9;
printf("after writing: tally[2] = %d\n", tally[2]);
free(tally);
return 0;
}tally[0] = 0
tally[1] = 0
tally[2] = 0
tally[3] = 0
after writing: tally[2] = 9calloc(4, sizeof(int)) and malloc(4 * sizeof(int)) ask for the same amount of space; only calloc
promises what is in it. It also does the multiplication itself, inside the library, where the case of a
count and a size whose product is too large to represent can be caught and turned into a NULL - and in
practice it is. Work out count * size yourself and hand the result to malloc, and an overflow there
has already silently become a small number before malloc ever sees it.
Sizing With sizeof on the Pointer
There is a small idiom worth adopting: sizeof *pointer rather than sizeof(type). It says "as big as
one of the things this pointer points at", so the size stays right even if the type later changes.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
size_t count = 6;
double *series = malloc(count * sizeof *series);
if (series == NULL) {
printf("allocation failed\n");
return 1;
}
for (size_t i = 0; i < count; i++) {
series[i] = 0.5 * (double) (i + 1);
}
double total = 0.0;
for (size_t i = 0; i < count; i++) {
printf("series[%zu] = %.2f\n", i, series[i]);
total += series[i];
}
printf("bytes requested: %zu\n", count * sizeof *series);
printf("total: %.2f\n", total);
free(series);
return 0;
}series[0] = 0.50
series[1] = 1.00
series[2] = 1.50
series[3] = 2.00
series[4] = 2.50
series[5] = 3.00
bytes requested: 48
total: 10.50sizeof *series is evaluated by the compiler from the type of series, so no pointer is dereferenced
and it is safe to write before series holds anything. Six doubles at eight bytes each is the 48 bytes
printed at the end.
Note also that count is a size_t and so is the loop counter. malloc takes a size_t, sizeof
produces a size_t, and keeping the arithmetic in that one unsigned type avoids the signed/unsigned
comparison warnings that otherwise pile up in this kind of code.
Resizing With realloc
realloc grows or shrinks an existing block, keeping as much of the contents as still fits. It may move
the block to do so, which means it returns a possibly different address - and that address must be
captured into a separate variable until you know it is not NULL.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *data = malloc(2 * sizeof *data);
if (data == NULL) {
printf("allocation failed\n");
return 1;
}
data[0] = 1;
data[1] = 2;
int *bigger = realloc(data, 4 * sizeof *data);
if (bigger == NULL) {
printf("could not grow the block\n");
free(data);
return 1;
}
data = bigger;
data[2] = 3;
data[3] = 4;
for (int i = 0; i < 4; i++) {
printf("%d\n", data[i]);
}
free(data);
return 0;
}1
2
3
4The two values written before the resize survived it, because realloc copies the old contents across.
The two new positions were untouched by realloc and had to be written before being read.
Writing data = realloc(data, ...) directly is a small classic mistake: if the call fails and returns
NULL, you have just overwritten the only pointer to the old block, which is still allocated and now
unreachable. Using a temporary, as above, keeps the old block recoverable.
Returning Memory From a Function
A pointer to a local variable is worthless once the function returns. A pointer to a heap block is not - which is how a function hands a whole array back to its caller.
#include <stdio.h>
#include <stdlib.h>
int *make_squares(int count) {
int *data = malloc((size_t) count * sizeof *data);
if (data == NULL) {
return NULL;
}
for (int i = 0; i < count; i++) {
data[i] = i * i;
}
return data;
}
int main(void) {
int *squares = make_squares(5);
if (squares == NULL) {
printf("could not allocate\n");
return 1;
}
for (int i = 0; i < 5; i++) {
printf("%d\n", squares[i]);
}
free(squares);
return 0;
}0
1
4
9
16make_squares allocates and fills; main uses and frees. That split raises the question C makes you
answer explicitly every time: who owns this block, and who frees it? Here the function's contract is
"the caller owns what I return", and main honours it. Write that contract down in a comment above any
function that returns allocated memory, because there is nothing in the type system to express it.
The same pattern works for strings:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *duplicate(const char *source) {
size_t length = strlen(source);
char *copy = malloc(length + 1);
if (copy == NULL) {
return NULL;
}
memcpy(copy, source, length + 1);
return copy;
}
int main(void) {
char *name = duplicate("forklift");
if (name == NULL) {
printf("out of memory\n");
return 1;
}
printf("%s\n", name);
printf("%zu characters in %zu bytes\n", strlen(name), strlen(name) + 1);
name[0] = 'F';
printf("%s\n", name);
free(name);
return 0;
}forklift
8 characters in 9 bytes
Forkliftlength + 1 appears twice, and both are the terminator. The allocation must be one byte longer than the
character count, and the copy must include that byte - memcpy(copy, source, length) would produce an
unterminated run of characters, and printing it with %s would read on past the end. The copy is
writable, which the literal it came from is not, so name[0] = 'F' is fine.
The Four Ways This Goes Wrong
A Memory Leak
You allocate and never free. The block stays reserved for the life of the program, unreachable and unusable.
void process(void) {
int *buffer = malloc(1000 * sizeof(int));
/* ... work ... */
/* no free - the address in buffer is lost when the function returns */
}One leak of a few kilobytes is harmless. A leak inside a loop, or in a long-running program, consumes
memory until allocation starts failing. Leaks do not make a program misbehave immediately, which is
exactly what makes them easy to ship. The habit that prevents most of them: write the free at the same
moment you write the malloc, before filling in the code between.
An early return is the classic way a free gets skipped:
int handle(int flag) {
char *buffer = malloc(64);
if (buffer == NULL) {
return 1;
}
if (flag == 0) {
return 2; /* leaks: buffer is never freed on this path */
}
free(buffer);
return 0;
}Every exit after a successful allocation must release it. Either free before each return, or use a
single cleanup point at the bottom that every path reaches.
A Dangling Pointer, and Use After Free
free releases the block but leaves your pointer holding the old address. Reading or writing through it
afterwards is use after free: undefined behaviour, and one of the most exploited classes of bug in
real software.
int *values = malloc(4 * sizeof *values);
/* ... use values ... */
free(values);
values[0] = 7; /* undefined behaviour: the block is not yours any more */There is no output to show for that, and this is the place to be most careful about what you conclude from an experiment. The memory has been handed back to the allocator, which is free to reuse it for the next request; whether your write appears to work, corrupts something else, or fails depends entirely on what the allocator did next. In this sandbox it will very often look like nothing happened at all. That appearance is not a result; the program has no defined behaviour from that line onward.
The fix is one line, and it is why every free in this lesson is followed by it:
free(values);
values = NULL;A null pointer can be tested. A dangling one cannot be distinguished from a valid one by any means available to your program.
A Double Free
Calling free twice on the same block is undefined behaviour, and it corrupts the allocator's own
bookkeeping rather than your data, so the damage typically surfaces somewhere else entirely. Setting the
pointer to NULL after freeing prevents this too, since free(NULL) is defined to do nothing.
The subtler version involves two pointers to one block:
int *a = malloc(sizeof *a);
int *b = a; /* two pointers, one block */
free(a);
free(b); /* undefined behaviour: the same block, freed twice */Copying a pointer does not copy the block. Deciding which pointer owns it - and freeing only through that one - is the discipline that avoids this.
Freeing Something That Was Not Allocated
free may only be given a pointer that came from malloc, calloc or realloc, or NULL.
int local = 5;
free(&local); /* undefined behaviour: this was never allocated */Passing a pointer into the middle of a block is the same mistake: after p++, free(p) is not freeing
what malloc gave you. Keep the original address if you intend to walk a block with a pointer.
A Worked Example
A function that builds a heap array from a starting value, a caller that uses it and frees it, and a single cleanup path.
#include <stdio.h>
#include <stdlib.h>
/* Returns a block of `count` doubles, or NULL. The caller owns it and must free it. */
double *make_series(int count, double start, double step) {
if (count <= 0) {
return NULL;
}
double *series = malloc((size_t) count * sizeof *series);
if (series == NULL) {
return NULL;
}
for (int i = 0; i < count; i++) {
series[i] = start + step * i;
}
return series;
}
int main(void) {
int count = 5;
double *series = make_series(count, 2.0, 0.25);
if (series == NULL) {
printf("could not build the series\n");
return 1;
}
double total = 0.0;
for (int i = 0; i < count; i++) {
printf("series[%d] = %.2f\n", i, series[i]);
total += series[i];
}
printf("count: %d\n", count);
printf("total: %.2f\n", total);
printf("mean: %.2f\n", total / count);
free(series);
series = NULL;
printf("freed: %d\n", series == NULL);
return 0;
}series[0] = 2.00
series[1] = 2.25
series[2] = 2.50
series[3] = 2.75
series[4] = 3.00
count: 5
total: 12.50
mean: 2.50
freed: 1make_series rejects a non-positive count before allocating, so its only two outcomes are "a valid block
you own" and "NULL". A caller therefore has exactly one thing to check, which is what makes a function
like this pleasant to use.
Every value in the series is a quarter step from the last, and quarters are exactly representable in
binary floating point, so these figures are exact rather than merely rounded for display. Had the step
been 0.1 the printed table would look just as tidy and the stored values would each be very slightly
off - a reason to prefer 0.25 in an example and to remember the variables lesson in real code.
The comment above the function is doing real work. Nothing in double *make_series(int, double, double)
tells a caller that they are responsible for freeing the result; only the comment does. In a larger
program that convention gets written down once for the whole codebase, and followed everywhere.
Common Mistakes
Not checking for NULL. The one error that turns a failed allocation into undefined behaviour.
Check before the first use, every time - especially here, where a bad dereference does not reliably
announce itself.
Confusing bytes with elements. malloc(5) gives five bytes, not five ints. Always multiply by
sizeof.
Reading a malloc block before writing it. Indeterminate contents. Use calloc, or write before you
read.
Losing the only pointer to a block. Reassigning the pointer, or letting it go out of scope, leaks the block. There is no way to get the address back.
Freeing twice, or freeing the wrong pointer. free exactly once, with exactly the address malloc
returned. Set the pointer to NULL afterwards and both problems mostly disappear.
Using a pointer after freeing it. Undefined behaviour, however normal the run looks. Assign NULL
immediately after the free so the mistake becomes testable.
data = realloc(data, n) on the pointer you would need for cleanup. On failure you lose the old
block. Use a temporary.
Forgetting the terminator's byte when allocating for a string. malloc(strlen(s)) is one byte too
small. It is always strlen(s) + 1.
Next Steps
In the C playground, write a function that allocates an array of n ints, fills
it with the first n even numbers, and returns it, then have main print and free it. Add the NULL
check on both sides. Then remove the free and confirm the program still appears to work perfectly -
which is the whole reason leaks are easy to miss, and worth seeing once with your own eyes.
The last lesson in this track is structs: how to group several related values into one type, give it a
short name with typedef, and build arrays of them.