C in 100 Seconds: Memory Leaks | Episode 22
Video: C in 100 Seconds: Memory Leaks — malloc Without free | Episode 22 by Taught by Celeste AI - AI Coding Coach
C Memory Leaks: malloc Without free
Every
mallocneeds a matchingfree. Forget once, leak a little. Forget in a loop, leak a lot. Tools like Valgrind and AddressSanitizer find them.
A memory leak in C is allocating memory and never freeing it. The bytes remain reserved until the program exits. For short scripts, no big deal. For long-running services or games, leaks pile up until you OOM.
The basic shape
#include <stdio.h>
#include <stdlib.h>
void leaky() {
int *data = malloc(100 * sizeof(int));
data[0] = 42;
printf("Used: %d\n", data[0]);
// forgot to free!
}
void clean() {
int *data = malloc(100 * sizeof(int));
data[0] = 42;
printf("Used: %d\n", data[0]);
free(data);
}
int main() {
leaky(); // 400 bytes leaked
clean(); // 400 bytes allocated then freed
return 0;
}
After leaky() returns, the local pointer data is destroyed — but the 400 bytes on the heap remain allocated forever (until process exit). Nothing in the program holds a reference, so it can never be freed.
Why it matters
For a one-shot program, the OS reclaims everything on exit. Leak doesn't really hurt.
For long-running:
- Daemons, servers, games. Leaking a little each request adds up. After hours, the process is fat; after days, OOM.
- Embedded systems. No virtual memory; physical RAM matters every byte.
- Graphics, video. Per-frame leaks bog down quickly.
A memory leak is a memory-correctness bug, even if the program runs fine for a while.
Common leak patterns
1. Lost pointer.
char *s = malloc(100);
s = strdup("hello"); // overwrites pointer; original allocation lost
The original 100-byte allocation has no pointer pointing to it. Permanently leaked.
2. Early return without free.
int *process() {
int *buf = malloc(1024);
if (something_failed()) {
return NULL; // leak: buf
}
// ... use buf, free, return ...
}
The return NULL skips the free. Fix: free before every return.
3. Loop allocations.
for (int i = 0; i < 1000; i++) {
char *line = read_line(); // line is malloc'd
process(line);
// forgot: free(line);
}
1000 leaked allocations. Always free at the end of each loop iteration.
4. Realloc trap.
nums = realloc(nums, new_size); // if realloc fails, nums = NULL, original lost
Episode 21 covered this. Use a temporary variable.
Detecting leaks
Valgrind (Linux/macOS):
gcc -g program.c -o program
valgrind --leak-check=full ./program
Output shows every leaked allocation with the file/line where it was allocated. Slow but thorough.
AddressSanitizer (built into GCC and Clang):
gcc -fsanitize=address -g program.c -o program
./program
Leaks reported on exit. Faster than Valgrind, integrated into the build, catches use-after-free and overflows too. The de-facto standard for modern C.
MallocStackLogging (macOS/iOS).
Windows: Visual Studio's CRT debug heap, or _CrtSetDbgFlag.
Defensive patterns
1. Always pair malloc with free. Match every allocation with a free in all code paths.
2. Single owner. Decide which function or struct owns a piece of memory. Only the owner frees.
3. Goto cleanup.
int do_thing() {
char *a = NULL, *b = NULL, *c = NULL;
int rc = -1;
a = malloc(...); if (!a) goto cleanup;
b = malloc(...); if (!b) goto cleanup;
c = malloc(...); if (!c) goto cleanup;
// ... use a, b, c ...
rc = 0;
cleanup:
free(a);
free(b);
free(c);
return rc;
}
goto cleanup is a C idiom for resource-cleanup paths. free(NULL) is well-defined (no-op), so freeing un-allocated pointers is safe — that's why we initialise to NULL.
4. Constructor/destructor pairs.
List *list_create() {
List *l = malloc(sizeof(*l));
l->data = malloc(...);
return l;
}
void list_destroy(List *l) {
if (l == NULL) return;
free(l->data);
free(l);
}
Wrap the allocation/cleanup so callers don't forget. Standard pattern in C libraries.
5. Set pointer to NULL after free.
free(p);
p = NULL; // double-frees turn into no-ops; UAF turns into NULL deref
Doesn't fix the leak, but converts later misuse from "silent corruption" into "crash you can debug."
The OS reclaims on exit
int main() {
void *p = malloc(1000000);
// never free
return 0;
}
When the process exits, all its memory is returned to the OS. The "leak" is irrelevant.
But this is a crutch. A program that "only leaks 1MB on exit" might really leak 1MB every iteration of a loop — Valgrind would still flag it.
Don't free what you didn't malloc
int x = 5;
free(&x); // CRASH — &x is a stack address, not heap
free is only for memory from malloc/calloc/realloc. Stack memory, string literals, statically-allocated globals — never free them.
Don't double-free
free(p);
free(p); // UNDEFINED BEHAVIOR
Double-freeing typically corrupts the heap and crashes later in a confusing place. Fix: set to NULL after free.
free(p);
p = NULL;
free(p); // free(NULL) is safe — no-op
Leaks vs lost references
A "leak" technically means no pointer can ever reach the allocation again. If you have a pointer to it, it's still findable — but if you've stored it somewhere you've forgotten about, that's also a problem.
Long-running programs sometimes have almost-leaks — references kept alive by mistake (e.g., a cache that grows forever). These don't show up in Valgrind because the memory is technically reachable. But they're functionally indistinguishable from leaks.
Common mistakes
Forgetting free in error paths. Most leaks come from "I freed in the happy path; the error path forgot."
Lost pointers. Reassigning a pointer without freeing the old allocation.
Double-frees. Set to NULL after free.
Freeing non-heap pointers. Crashes immediately.
Treating leaks as "fixable later." They accumulate. Run sanitizers regularly.
Skipping NULL checks on malloc. A NULL deref isn't a leak per se, but adjacent bug.
What's next
Episode 23: structs. Group related data into named fields. The C version of "objects."
Recap
Every malloc needs exactly one free. Forget = leak. Detection: Valgrind, AddressSanitizer (gcc -fsanitize=address). Patterns to prevent: pair malloc/free, single owner, goto cleanup for multi-resource error handling, constructor/destructor pairs, set to NULL after free. Don't free non-heap memory; don't double-free. OS reclaims on exit, but that's not a license to be sloppy in long-running code.
Next episode: structs.