Dynamic memory basics

Table of contents
  1. Dynamic memory basics
    1. The stack and heap
    2. Allocating memory with malloc()
    3. Checking that allocation succeeded
    4. Using the allocated memory
    5. Releasing memory with free()
    6. Complete example
    7. Common mistakes
      1. Memory leak
      2. Using memory after free()
      3. Freeing memory twice
    8. Dynamic memory in embedded systems
    9. How other languages manage memory

So far, the size of our arrays has been fixed when the program is written:

int values[10];

Sometimes we do not know how much memory is needed until the program is running.

For example, the number of values might come from:

  • user input
  • a file
  • data received from another system

Dynamic memory allocation allows the program to request memory while it is running.

The stack and heap

The memory used by a program is divided into several areas. Two that we need to know about are the stack and the heap.

Local variables normally use the stack:

void example(void)
{
    int value = 10;
    int numbers[20];
}

Allocating local variables on the stack is normally fast and their memory is released automatically when the function returns. Allocating memory on the heap using malloc() has some additional overhead, and the program must later release the memory using free().

A simple way to think about the difference is:

Stack: automatic, normally fast, fixed lifetime
Heap:  flexible size and lifetime, manually managed

You can see that the flexibility of using the Heap comes at the cost of us having to control the memory ourselves - massively increasing the chance of us doing something wrong!

Allocating memory with malloc()

The malloc() function requests a block of memory from the heap, and is provided by <stdlib.h>. For example, this allocates enough space for ten integers:

#include <stdlib.h>
int *values = malloc(10 * sizeof(int));

malloc() returns the address of the allocated memory. We store that address in a pointer.

Using sizeof makes sure the correct number of bytes is requested:

10 * sizeof(int)

A common alternative is (although I find it quite confusing tbh):

int *values = malloc(10 * sizeof(*values));

Here, sizeof(*values) means the size of the type that values points to. This avoids repeating int.

Checking that allocation succeeded

Memory allocation can fail. If malloc() cannot provide the requested memory, it returns NULL.

Just like we saw with pointers or file pointers, always check the result before using the memory:

int *values = malloc(10 * sizeof(*values));

if (values == NULL) {
    return 1;
}

Do not access the memory unless the pointer has been checked, it could cause crashes and chaos.

Using the allocated memory

The allocated block can be used like an array:

for (size_t i = 0; i < 10; i++) {
    values[i] = (int)i;
}

The memory returned by malloc() is not initialised. It may contain old values left in that part of memory.

Set each element before reading it:

for (size_t i = 0; i < 10; i++) {
    values[i] = 0;
}

Releasing memory with free()

Memory allocated using malloc() remains allocated until it is released using free():

free(values);

After calling free(), the memory no longer belongs to the program.

Do not use the pointer to access that memory again:

free(values);

values[0] = 10;    // Invalid

It can be useful to set the pointer to NULL after freeing it:

free(values);
values = NULL;

This does not change what free() does, but it makes accidental reuse easier to spot. Basically, protect yourself from yourself.

Complete example

This program asks how many values are required, allocates an array and fills it with numbers:

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    size_t count;

    printf("How many exciting numbers do you need? ");

    if (scanf("%zu", &count) != 1 || count == 0) {
        printf("Invalid number\n");
        return 1;
    }

    int *values = malloc(count * sizeof(*values));

    if (values == NULL) {
        printf("Could not allocate memory\n");
        return 1;
    }

    for (size_t i = 0; i < count; i++) {
        values[i] = (int)(i + 1);
    }

    for (size_t i = 0; i < count; i++) {
        printf("values[%zu] = %d\n", i, values[i]);
    }

    free(values);
    values = NULL;

    return 0;
}

The amount of memory depends on the value entered while the program is running:

malloc(count * sizeof(*values))

The program follows the main dynamic-memory sequence:

allocate -> check -> use -> free

Common mistakes

I have made each of these several times.

Memory leak

A memory leak occurs when memory is allocated but never freed:

int *values = malloc(10 * sizeof(*values));

if (values == NULL) {
    return 1;
}

// Use values

return 0;    // values was not freed

The memory remains allocated until the program ends.

Release it when it is no longer needed:

free(values);

Using memory after free()

After freeing the memory, the pointer must not be dereferenced:

free(values);

printf("%d\n", values[0]);    // Invalid

The pointer still contains an address, but the memory at that address no longer belongs to the program.

Freeing memory twice

Do not free the same allocation twice:

free(values);
free(values);    // Invalid

Setting the pointer to NULL after freeing can help:

free(values);
values = NULL;

Calling free(NULL) is safe.

Dynamic memory in embedded systems

Dynamic memory can be useful, but it is often avoided or used very carefully on microcontrollers. The available heap may be small, like 10 kB small, and repeatedly allocating and freeing blocks can fragment the memory.

For embedded systems, it is normally safer to allocate a fixed-size array large enough for the maximum amount of data we expect:

#define MAX_VALUES 100

int values[MAX_VALUES];
size_t value_count = 0;

The array always has space for 100 values, while value_count records how many elements are currently being used.

If the array becomes full, the program must decide what to do. It could reject new values, process and clear the existing values, or overwrite the oldest value. The important point is that the maximum memory use is known when the program is compiled.

Dynamic memory is still useful in some situations, but fixed-size buffers often give more predictable behaviour on a microcontroller.

The next page covers calloc(), resizing memory with realloc(), and managing growing arrays.

How other languages manage memory

C gives the programmer direct control over dynamic memory using malloc() and free().

Other languages may manage this automatically. For example, Java, Python and C# reclaim unused memory using automatic memory-management systems. Rust uses ownership rules checked by the compiler, while modern C++ often uses objects that release their memory automatically.

C gives us more direct control, but also makes us responsible for avoiding memory leaks, double frees and use-after-free errors. Meaning many more chances for segmentation faults, hooray!


This site uses Just the Docs, a documentation theme for Jekyll.