CodeMIRAI
0 XP

Dynamic memory

3 min read 5 runnable examples Quiz included

So far every variable had a size fixed when you wrote the program, and disappeared when its function returned. Dynamic memory is memory you ask for while the program runs, from an area called the heap. It lasts until you hand it back.

high addresseslow addressesStacklocal variables, function calls↓ grows downfree↑ grows upHeapmalloc / free, you manage itGlobals and staticlive for the whole programCodethe compiled program
Locals live on the stack and vanish when their function returns; malloc'd memory lives on the heap until you free it.

You need it when the size isn't known in advance (a list as long as the user wants) or when data must outlive the function that created it.

malloc and free

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

int main() {
    int n = 5;
    int *squares = malloc(n * sizeof *squares);
    if (squares == NULL) {
        printf("Out of memory\n");
        return 1;
    }
    for (int i = 0; i < n; i++) squares[i] = i * i;
    for (int i = 0; i < n; i++) printf("%d ", squares[i]);
    printf("\n");
    free(squares);
    return 0;
}
Output
0 1 4 9 16 
  • malloc(bytes) reserves that many bytes and returns a pointer to them, from <stdlib.h>.
  • n * sizeof *squares is "n of whatever squares points to", which stays right even if you later change int to long.
  • malloc returns NULL when it can't allocate. Check before using the pointer.
  • The pointer works like an array: squares[i].
  • free returns the memory. Every malloc needs exactly one free.

Sizing it at run time

This example's input is 4, then 10 20 30 40:

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

int main() {
    int n;
    printf("How many numbers? ");
    if (scanf("%d", &n) != 1 || n <= 0) return 1;
    double *values = malloc(n * sizeof *values);
    if (!values) return 1;
    double sum = 0;
    for (int i = 0; i < n; i++) {
        scanf("%lf", &values[i]);
        sum += values[i];
    }
    printf("\nAverage of %d numbers: %.2f\n", n, sum / n);
    free(values);
    return 0;
}
Output
How many numbers? 
Average of 4 numbers: 25.00

The array is exactly as long as the user asked for, which a fixed-size array can't promise.

calloc and realloc

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

int main() {
    int *counts = calloc(5, sizeof *counts);         // five ints, all zero
    if (!counts) return 1;
    for (int i = 0; i < 5; i++) printf("%d ", counts[i]);
    printf("\n");

    int *bigger = realloc(counts, 8 * sizeof *counts);   // grow to eight
    if (!bigger) { free(counts); return 1; }
    counts = bigger;
    for (int i = 5; i < 8; i++) counts[i] = i * 10;      // the new part is not zeroed
    for (int i = 0; i < 8; i++) printf("%d ", counts[i]);
    printf("\n");
    free(counts);
    return 0;
}
Output
0 0 0 0 0 
0 0 0 0 0 50 60 70 
  • calloc(count, size) allocates and fills with zeros. malloc leaves whatever bytes were there.
  • realloc(p, bytes) resizes a block, keeping its contents. It may move the data, so use the pointer it returns.
  • Store realloc's result in a new variable first: if it fails it returns NULL, and writing that over your only pointer would lose the original block.

An array that grows

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

typedef struct {
    int *items;
    int size;
    int capacity;
} IntList;

int push(IntList *list, int value) {
    if (list->size == list->capacity) {
        int cap = list->capacity ? list->capacity * 2 : 2;
        int *grown = realloc(list->items, cap * sizeof *grown);
        if (!grown) return 0;
        list->items = grown;
        list->capacity = cap;
        printf("(grew to %d)\n", cap);
    }
    list->items[list->size++] = value;
    return 1;
}

int main() {
    IntList list = {NULL, 0, 0};
    for (int i = 1; i <= 5; i++) push(&list, i * i);
    for (int i = 0; i < list.size; i++) printf("%d ", list.items[i]);
    printf("\nsize %d, capacity %d\n", list.size, list.capacity);
    free(list.items);
    return 0;
}
Output
(grew to 2)
(grew to 4)
(grew to 8)
1 4 9 16 25 
size 5, capacity 8

Doubling the capacity each time it fills keeps the number of realloc calls small even for long lists. This is how C++'s std::vector works inside. realloc(NULL, bytes) behaves like malloc, which is why the list can start with items = NULL.

Strings on the heap

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

char *join(const char *first, const char *last) {
    size_t len = strlen(first) + 1 + strlen(last) + 1;   // a space, and the '\0'
    char *s = malloc(len);
    if (!s) return NULL;
    strcpy(s, first);
    strcat(s, " ");
    strcat(s, last);
    return s;                     // the caller must free it
}

int main() {
    char *name = join("Ada", "Lovelace");
    if (!name) return 1;
    printf("%s (%zu characters)\n", name, strlen(name));
    free(name);
    return 0;
}
Output
Ada Lovelace (12 characters)

A function can return heap memory because it outlives the call. Two details matter: allocate one extra byte for the terminating '\0', and make it clear who frees the result.

Returning the address of a local array is the classic mistake. The array disappears when the function returns, leaving a pointer to memory that's no longer yours:

C
char *greeting(void) {
    char text[] = "hello";
    return text;              // wrong: text dies when the function returns
}

The four classic mistakes

  • Memory leak: losing the last pointer to a block without freeing it. The memory stays reserved until the program ends.
  • Use after free: reading or writing through a pointer after free. The memory may already belong to something else.
  • Double free: calling free twice on the same block, which can corrupt the heap.
  • Off by one: forgetting the + 1 for a string's '\0', so strcpy writes past the end.
C
free(p);
p = NULL;     // a habit that turns a later accidental use into an obvious crash

Tools like Valgrind and the compilers' AddressSanitizer (gcc -fsanitize=address) find these bugs for you.

Finished this lesson?Mark it complete to earn +20 XP and move your progress on.