Menu

Memory Leaks in C: How They Happen and How to Find Them

What a leak really is, the three ways C programs produce them, the ownership discipline that prevents them, and how to find the rest with valgrind and -fsanitize=address - ending with a leaky program fixed step by step.

This page includes runnable editors - edit, run, and see output instantly.

A memory leak is not memory that vanished. It is memory that is still yours, still reserved, and that you have lost the ability to hand back - because the last pointer to it is gone. Nothing crashes. The program keeps running, a little heavier each time, until something eventually fails somewhere unrelated.

This page covers how leaks arise, the discipline that prevents most of them, and the two tools that find the rest.

What a Leak Looks Like

Each iteration overwrites block with a fresh pointer. The previous block is still allocated; no variable holds its address; it can never be freed. Three iterations lose twelve kilobytes. A server doing this once per request loses it forever, at whatever rate requests arrive.

The fix is one line - free(block); at the end of the body - but recognizing where it belongs is the actual skill.

How Leaks Happen

1. The lost pointer

Any assignment to a pointer that still holds the only reference to a live block leaks it.

char *name = malloc(32);
name = malloc(64);     /* the first 32 bytes are now unreachable */

The loop above is the same bug wearing a loop. So is reassigning a struct field, and so is the realloc shorthand from calloc and realloc:

p = realloc(p, n);     /* on failure: p becomes NULL, old block orphaned */

2. The early return

Every path out of a function has to release what the function has already taken. The one that gets forgotten is always an error path.

The happy path is correct and the error path leaks, which is why this survives testing: the failing branch almost never runs during development. The fix is a single cleanup section each path jumps to:

This is the one use of goto that experienced C programmers actively recommend. It works because every pointer starts at NULL and free(NULL) is a no-op, so a single exit block is correct no matter how far the function got.

3. Unclear ownership

The subtlest leaks are not coding errors at all - they are two functions disagreeing about whose job it was.

char *build_message(void);   /* does the caller free this? */
void  store(char *text);     /* does store take ownership? */

If build_message returns allocated memory and store copies it, the caller must free. If store keeps the pointer, the caller must not. Nothing in the code says which, so one of the two assumptions gets made twice - and you get either a leak or a double free.

The remedy is a convention, stated in a comment beside every function that allocates:

/* Returns a newly allocated string; the caller must free it. */
char *build_message(void);

/* Takes ownership of 'text'; it will be freed by store_free(). */
void store(char *text);

Write the rule at the function, not in a design document. It is the single highest-value habit in C memory management.

The Ownership Discipline

Four rules cover nearly everything:

  1. Every allocation has exactly one owner - one piece of code responsible for freeing it.
  2. Pair each allocating function with a releasing one. vec_init / vec_free, config_load / config_free. The symmetry makes a missing call visible.
  3. Free in the same layer that allocated, unless the function's comment explicitly transfers ownership.
  4. Set a pointer to NULL after freeing it, so a later accidental use crashes at the fault site instead of corrupting the heap quietly.

Finding Leaks: valgrind

On Linux, valgrind needs no recompilation, though debug symbols make the report readable:

gcc -g -O0 program.c -o program
valgrind --leak-check=full --show-leak-kinds=all ./program

For the leaking loop at the top of this page, the report ends with something like:

==12345== HEAP SUMMARY:
==12345==     in use at exit: 12,000 bytes in 3 blocks
==12345==   total heap usage: 3 allocs, 0 frees, 12,000 bytes allocated
==12345==
==12345== 12,000 bytes in 3 blocks are definitely lost in loss record 1 of 1
==12345==    at 0x4C2FB0F: malloc (vg_replace_malloc.c:299)
==12345==    by 0x108671: main (program.c:6)
==12345==
==12345== LEAK SUMMARY:
==12345==    definitely lost: 12,000 bytes in 3 blocks

Read it from the bottom. "definitely lost" means no pointer to the block existed at exit - a real leak. The stack trace names the line of the malloc that created it, not the line where it was lost, which is usually enough to find the missing free.

Two other categories appear:

  • indirectly lost - blocks reachable only through a block that was itself lost, such as the elements of a leaked linked list. Fix the "definitely lost" one and these disappear.
  • still reachable - allocated at exit but with a live pointer, typically a global cache. Not a leak in the dangerous sense, but worth freeing so the report stays empty.

Valgrind also catches reads of uninitialized memory and writes past the end of a block, which is often how you discover the bug behind the leak.

Finding Leaks: AddressSanitizer

AddressSanitizer is built into GCC and Clang, runs far faster than valgrind, and works where valgrind does not (including current macOS):

gcc -g -fsanitize=address -fno-omit-frame-pointer program.c -o program
./program

The leak report prints at exit automatically:

=================================================================
==12345==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 12000 byte(s) in 3 object(s) allocated from:
    #0 0x7f... in malloc
    #1 0x1086... in main program.c:6

SUMMARY: AddressSanitizer: 12000 byte(s) leaked in 3 allocation(s).

ASan also turns use-after-free and heap buffer overflows into immediate, clearly labeled aborts rather than mysterious corruption later. Build your test runs with it on; build releases with it off, since it costs memory and speed.

If leak detection does not fire on your platform, set ASAN_OPTIONS=detect_leaks=1 in the environment before running.

Fixing a Leaky Program Step by Step

Here is a small program with three separate leaks:

Valgrind reports three "definitely lost" records with three different line numbers. Fixed one at a time:

Fix 1 is the ownership comment made real: shout allocates, main frees. Fix 2 removes the doubled allocation entirely rather than freeing the first - the simpler code is also the correct code. Fix 3 adds the missing free on the early return; with more allocations in play, the single cleanup: label shown earlier scales better than repeating the frees.

Habits That Prevent Leaks

  • Write the free immediately after writing the malloc, then fill the code in between.
  • Give every allocating function a matching freeing function.
  • State ownership in a comment on any function that returns or takes a pointer it allocated.
  • Use one cleanup: exit block in functions holding several allocations.
  • Run your tests under -fsanitize=address as a matter of course, not only when something looks wrong.
  • Treat "definitely lost: 0 bytes" as part of a passing test run.

Frequently Asked Questions

What is a memory leak in C?

Memory you allocated with malloc that you can no longer free, because nothing in the program still points at it. The block stays reserved for the life of the process. It is not a crash - the program keeps working, just using more memory on every pass until it eventually runs out.

How do I find memory leaks in C?

Run the program under valgrind: valgrind --leak-check=full ./program. It reports every block still allocated at exit with the stack trace of the malloc that created it. On macOS or where valgrind is unavailable, compile with -fsanitize=address and the same report comes out at exit.

What causes memory leaks in C?

Three patterns cover almost all of them: overwriting the only pointer to a block (including p = realloc(p, n) on failure), returning early from a function that has already allocated, and unclear ownership - two functions each assuming the other frees it, so neither does.

Do memory leaks matter if the program exits anyway?

For a program that runs once and exits, the operating system reclaims everything, so the practical impact is nil. It matters for anything long-running - a server, a game loop, a daemon - where a leak per request grows without bound. Free consistently anyway: a leak report is noise that hides the ones that do matter.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED