Dynamic memory · about 55 min

Ownership and cleanup patterns

Why this matters

malloc and free are easy in a ten-line program. In a real program, pointers are passed between functions, stored in arrays, returned from libraries, and errors can happen halfway through building something. The question "who is responsible for freeing this, and when?" has to have a clear answer for every allocation, or you get leaks (nobody frees it) and double frees (two parts of the program both think they should). C has no language feature for this; it is a discipline. This lesson covers the conventions experienced C programmers use, and the goto cleanup pattern that the Linux kernel, Git and CPython use to handle errors without leaking.

Ownership

The owner of a heap block is the part of the program responsible for freeing it. Every block should have exactly one owner at any moment. Three things can happen to a pointer when it crosses a function boundary:

situationmeaningexample
transfer outthe function allocates and gives you ownership; you must freestrdup, make_names below
borrowyou get a pointer to use temporarily; the owner keeps itstrlen(s), getenv("HOME")
transfer inyou hand ownership to the function; you must not free it any morea function that stores your buffer in a list it later frees

Because the type char * looks the same in all three cases, the contract lives in the documentation. Good C code states it in the comment above every function that deals in pointers. Two conventions help:

Nested ownership: an array of strings

An array of strings built on the heap is one allocation (the array of pointers) that owns other allocations (the strings):

  names (char **)
    |
    v
  +---------+        +---+---+---+---+---+---+---+
  | names[0]| -----> | n | a | m | e | - | 0 |\0 |   each string: its own malloc
  +---------+        +---+---+---+---+---+---+---+
  | names[1]| -----> "name-1"
  +---------+
  | names[2]| -----> "name-2"
  +---------+
   one malloc for the array of 3 pointers

Freeing only names leaks all three strings (valgrind calls them "indirectly lost": only reachable through a lost block). Freeing the strings after names reads freed memory. The order is: free the children, then the parent.

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

/* Returns a heap copy of s, or NULL. The caller owns the result. */
char *copy_string(const char *s)
{
    char *p = malloc(strlen(s) + 1);
    if (p != NULL)
        strcpy(p, s);
    return p;
}

/* Returns a heap array of n heap strings "name-0", "name-1", ...
   The caller owns the array and every string: release with free_names. */
char **make_names(int n)
{
    char **names = calloc(n, sizeof *names);
    if (names == NULL)
        return NULL;
    for (int i = 0; i < n; i++) {
        char tmp[32];
        snprintf(tmp, sizeof tmp, "name-%d", i);
        names[i] = copy_string(tmp);
        if (names[i] == NULL) {
            for (int j = 0; j < i; j++)      /* undo the partial work */
                free(names[j]);
            free(names);
            return NULL;
        }
    }
    return names;
}

void free_names(char **names, int n)
{
    if (names == NULL)
        return;
    for (int i = 0; i < n; i++)
        free(names[i]);
    free(names);
}

int main(void)
{
    char **names = make_names(3);
    if (names == NULL)
        return 1;
    for (int i = 0; i < 3; i++)
        printf("%s\n", names[i]);
    free_names(names, 3);
    return 0;
}
name-0
name-1
name-2

snprintf(buf, size, format, ...) works like printf but writes into the character array buf, never more than size bytes including the terminator. It is the safe way to build a formatted string.

Note what make_names does when an allocation fails halfway: it frees exactly what it already made and returns NULL, so the caller gets either a complete result or nothing. That is the all-or-nothing rule for constructors, and it saves every caller from having to clean up a half-built object.

Error paths: the goto cleanup pattern

The hard case is a function that acquires several resources, any step of which can fail. The naive version frees by hand at every early return, and every new resource makes each return longer:

a = malloc(...);
if (a == NULL) return -1;
b = malloc(...);
if (b == NULL) { free(a); return -1; }
c = malloc(...);
if (c == NULL) { free(b); free(a); return -1; }
/* ... and each later error repeats all three frees ... */

Sooner or later one path forgets one free. The standard C solution uses goto to jump to a single cleanup section at the end:

int process(int n)
{
    int status = -1;          /* pessimistic: failure unless we reach the end */
    int *a = NULL, *b = NULL, *c = NULL;

    a = malloc(n * sizeof *a);
    if (a == NULL)
        goto cleanup;
    b = malloc(n * sizeof *b);
    if (b == NULL)
        goto cleanup;
    c = malloc(n * sizeof *c);
    if (c == NULL)
        goto cleanup;

    /* ... real work; any failure: goto cleanup ... */

    status = 0;               /* success */

cleanup:
    free(c);
    free(b);
    free(a);
    return status;
}

goto label; jumps to the line marked label: in the same function. It has a bad reputation because jumping around arbitrarily makes code unreadable, but this one disciplined use, jumping forward to cleanup, is idiomatic C and makes resource handling far more reliable. It works because of three details:

  1. Every pointer is initialized to NULL before the first goto, so cleanup never frees garbage.
  2. free(NULL) does nothing, so the cleanup code does not need to know how far the function got.
  3. The status starts as failure and becomes success only on the last line before the label, so a forgotten error code cannot report success.

One rule: a goto must not jump forward past the declaration of a variable-length array, and it is clearer not to jump past any declarations that are used in cleanup. Declare the resources at the top.

Other habits that prevent bugs

Common mistakes

Recap

Going further: search the Git or CPython source for goto cleanup or goto error to see the pattern at scale. In the next module you will bundle related data into structs, and create/destroy pairs become the natural way to manage them.

Exercises

Exercise 1 · quiz

Who frees what?

A library documents these functions:

/* Returns a new string. The caller owns it and must free() it. */
char *read_title(void);

/* Returns a pointer into the library's internal table. Valid until
   the next call. Do not free. */
const char *error_message(int code);

/* Takes ownership of buf: the library frees it when done. */
void queue_send(char *buf);

Which uses are correct? Select all that apply.

Answer
  • char *t = read_title(); printf("%s\n", t); free(t);

  • char *b = malloc(64); queue_send(b); b = NULL;

read_title transfers ownership to you, so you free it. error_message lends you a pointer (you borrow it): freeing memory you do not own is an invalid free, or worse, corrupts the library's table. queue_send takes ownership, so after the call the buffer is no longer yours; freeing it as well is a double free once the library frees it. Setting your copy to NULL after handing it over is a good habit that documents the transfer.

Exercise 2 · write code

Clean up on every path

The program reads a count n and then n integers, and prints their sum and their maximum. It allocates two arrays. When the input is bad partway through, it returns early without freeing what it already allocated, which valgrind reports as a leak.

Restructure main with the goto cleanup pattern so every path frees everything:

The output must stay exactly as described; the check runs every case under valgrind, including the error paths.

File: main.c

Starting point

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

int main(void)
{
    int n;
    if (scanf("%d", &n) != 1 || n <= 0) {
        printf("bad count\n");
        return 1;
    }

    int *values = malloc(n * sizeof *values);
    if (values == NULL)
        return 1;
    int *running = malloc(n * sizeof *running);   /* running sums */
    if (running == NULL)
        return 1;                                  /* leaks values */

    for (int i = 0; i < n; i++) {
        if (scanf("%d", &values[i]) != 1) {
            printf("bad value at %d\n", i + 1);
            return 1;                              /* leaks both */
        }
        running[i] = values[i] + (i > 0 ? running[i - 1] : 0);
    }

    int max = values[0];
    for (int i = 1; i < n; i++)
        if (values[i] > max)
            max = values[i];
    printf("sum %d max %d\n", running[n - 1], max);
    return 0;                                      /* leaks both */
}

Example

input

3
4 9 2

expected output

sum 15 max 9

Example

input

4
1 2 x 4

expected output

bad value at 3
Hint 1

Declare int *values = NULL; int *running = NULL; int status = 1; at the top of main, before any goto.

Hint 2

Replace every early return 1; with goto cleanup;, set status = 0; after the success printf.

Hint 3

At the end: cleanup: free(running); free(values); return status;. Because free(NULL) does nothing, the same lines work no matter how far the program got.

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

int main(void)
{
    int status = 1;          /* assume failure until we get to the end */
    int *values = NULL;
    int *running = NULL;

    int n;
    if (scanf("%d", &n) != 1 || n <= 0) {
        printf("bad count\n");
        goto cleanup;
    }

    values = malloc(n * sizeof *values);
    if (values == NULL)
        goto cleanup;
    running = malloc(n * sizeof *running);
    if (running == NULL)
        goto cleanup;

    for (int i = 0; i < n; i++) {
        if (scanf("%d", &values[i]) != 1) {
            printf("bad value at %d\n", i + 1);
            goto cleanup;
        }
        running[i] = values[i] + (i > 0 ? running[i - 1] : 0);
    }

    int max = values[0];
    for (int i = 1; i < n; i++)
        if (values[i] > max)
            max = values[i];
    printf("sum %d max %d\n", running[n - 1], max);
    status = 0;

cleanup:
    free(running);           /* free(NULL) is fine */
    free(values);
    return status;
}

Exercise 3 · write code

An allocation that owns allocations

Write two functions that form a create/destroy pair:

char **split_words(const char *s, int *count);
void free_words(char **words, int count);

split_words splits s into words separated by one or more spaces. It returns a newly allocated array of newly allocated strings (one per word) and stores the number of words in *count. For " to be or not " it returns {"to", "be", "or", "not"} with *count == 4. For a string with no words, it returns an array of zero words (it may be NULL or a valid pointer from malloc; free_words must accept either) and sets *count to 0.

free_words releases every string and then the array.

If any allocation fails, split_words must free whatever it already allocated and return NULL. Only spaces (' ') separate words.

Your main is not checked; the test calls the functions directly under valgrind. Optional: make your main print each word on its own line, for example for "hello C world".

File: main.c

Starting point

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

char **split_words(const char *s, int *count)
{
    *count = 0;
    return NULL;
}

void free_words(char **words, int count)
{
}

int main(void)
{
    int n;
    char **w = split_words("hello C world", &n);
    for (int i = 0; i < n; i++)
        printf("%s\n", w[i]);
    free_words(w, n);
    return 0;
}
Hint 1

Two passes are simplest: count the words first (a word starts at a non-space whose previous character is a space or which is at index 0), allocate the array of char pointers, then copy.

Hint 2

To copy a word: remember its start index, advance to the next space or '\0', then malloc(len + 1), memcpy, and terminate.

Hint 3

free_words: free each words[i] for i < count, then free(words). In split_words, if a word's malloc fails, call free_words(words, words_made_so_far) and return NULL.

Reference solution
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void free_words(char **words, int count)
{
    if (words == NULL)
        return;
    for (int i = 0; i < count; i++)
        free(words[i]);
    free(words);
}

char **split_words(const char *s, int *count)
{
    /* first pass: count the words so the array can be allocated once */
    int n = 0;
    for (size_t i = 0; s[i] != '\0'; i++)
        if (s[i] != ' ' && (i == 0 || s[i - 1] == ' '))
            n++;

    *count = 0;
    char **words = malloc((n > 0 ? n : 1) * sizeof *words);
    if (words == NULL)
        return NULL;

    /* second pass: copy each word */
    int w = 0;
    size_t i = 0;
    while (s[i] != '\0') {
        while (s[i] == ' ')
            i++;
        if (s[i] == '\0')
            break;
        size_t start = i;
        while (s[i] != '\0' && s[i] != ' ')
            i++;
        size_t len = i - start;
        char *word = malloc(len + 1);
        if (word == NULL) {
            free_words(words, w);      /* only the w words made so far */
            return NULL;
        }
        memcpy(word, s + start, len);
        word[len] = '\0';
        words[w++] = word;
    }
    *count = w;
    return words;
}

int main(void)
{
    int n;
    char **w = split_words("hello C world", &n);
    if (w == NULL)
        return 1;
    for (int i = 0; i < n; i++)
        printf("%s\n", w[i]);
    free_words(w, n);
    return 0;
}