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:
| situation | meaning | example |
|---|---|---|
| transfer out | the function allocates and gives you ownership; you must free | strdup, make_names below |
| borrow | you get a pointer to use temporarily; the owner keeps it | strlen(s), getenv("HOME") |
| transfer in | you hand ownership to the function; you must not free it any more | a 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:
constfor borrowing. A parameterconst char *ssays "I will only read this, I will not keep it, and I will certainly not free it".- Paired names. If a function creates something that needs more than one
free, provide a matching destroy function:make_names/free_names,fopen/fclose, and later in the tracklist_new/list_free. The caller then only has to remember "every create gets a destroy".
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:
- Every pointer is initialized to
NULLbefore the firstgoto, so cleanup never frees garbage. free(NULL)does nothing, so the cleanup code does not need to know how far the function got.- 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
- Set pointers to
NULLafter freeing when the variable stays in scope. A secondfreeis then harmless and a mistaken dereference crashes immediately instead of corrupting memory. - Free in the reverse order of allocation, especially when later resources depend on earlier ones.
- Keep allocation and release close in the code, ideally in the same function or in a create/destroy pair. A
mallocin one file and itsfreein another is hard to audit. - Run every path under valgrind, including the error paths. Leaks love error paths because they rarely run.
Common mistakes
- Freeing borrowed memory, such as the result of
getenvor a string literal:free(): invalid pointerand an abort, or heap corruption reported later. - Two owners: storing the same pointer in two places and freeing it from both. glibc says
free(): double free detected in tcache 2. - Freeing the parent first (
free(names)thennames[i]): a read of freed memory. valgrind:Invalid read of size 8 ... inside a block of size 24 free'd. - Uninitialized pointers before a goto:
int *a; ... goto cleanup; ... cleanup: free(a);frees a garbage address. gcc may warn'a' may be used uninitialized. - Returning a half-built object on failure, leaving the caller to guess what to free.
Recap
- Every heap block has one owner responsible for freeing it. Functions transfer ownership out, lend (borrow), or take ownership in, and the comment above each function must say which.
constparameters signal borrowing; create/destroy pairs hide multi-step cleanup.- Nested allocations are freed children first, then the parent.
- Constructors are all-or-nothing: on failure, undo partial work and return
NULL. goto cleanupwithNULL-initialized resources and a pessimistic status gives a single, reliable exit path.
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.