Skip to content

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.

C
#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;
}
Output
numbers[0] = 100
numbers[1] = 200
numbers[2] = 300
numbers[3] = 400
numbers[4] = 500
released

Five 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:

C
#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;
}
Output
tally[0] = 0
tally[1] = 0
tally[2] = 0
tally[3] = 0
after writing: tally[2] = 9

calloc(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.

C
#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;
}
Output
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.50

sizeof *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.

C
#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;
}
Output
1
2
3
4

The 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.

C
#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;
}
Output
0
1
4
9
16

make_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:

C
#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;
}
Output
forklift
8 characters in 9 bytes
Forklift

length + 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.

C
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:

C
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.

C
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:

C
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:

C
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.

C
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.

C
#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;
}
Output
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: 1

make_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.