Part of C in 100s

C in 100 Seconds: Memory Leaks | Episode 22

Celest KimCelest Kim

Video: C in 100 Seconds: Memory Leaks — malloc Without free | Episode 22 by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

C Memory Leaks: malloc Without free

Every malloc needs a matching free. 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.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.