Detecting Leaks with Valgrind
Compiler warnings and sanitizers catch a lot of mistakes at compile time or while a specific code path is running, but some bugs only show up when you watch a program's actual memory behavior over its full lifetime. Valgrind is a dynamic analysis tool suite built exactly for that job. Instead of reading your source code, it runs your compiled program inside a synthetic CPU it controls, watching every memory access as it happens.
Running a Program Under Valgrind
Compile normally, ideally with debug symbols so Valgrind can show you source line numbers, then prefix the run with valgrind.
gcc -g -Wall -Wextra -o myprogram myprogram.c valgrind ./myprogram
The -g flag matters: without debug symbols, Valgrind can still detect problems, but its report will show raw addresses instead of the file and line number where the bad access happened.
What Memcheck Catches
Memory leaks — memory allocated with
malloc/calloc/reallocthat is never freed before the program exits.Use-after-free — reading or writing memory through a pointer after it has already been passed to
free.Reading uninitialized memory — using a value before it has ever been written, which can silently produce different results on different runs.
Invalid reads/writes — buffer overflows and underflows, such as writing one element past the end of an array.
Double frees — calling
freetwice on the same pointer.Mismatched allocation/deallocation — e.g. allocating with
newand freeing withfreein mixed C/C++ code.
A Worked Example
Here is a small program with a deliberate, easy-to-miss leak: it allocates a buffer inside a function and returns without freeing it.
#include <stdlib.h>
void build_report(void) {
int *scores = malloc(10 * sizeof(int)); /* never freed */
scores[0] = 100;
}
int main(void) {
build_report();
return 0;
}Running it under Valgrind produces a report like this:
==12345== HEAP SUMMARY: ==12345== in use at exit: 40 bytes in 1 blocks ==12345== total heap usage: 1 allocs, 0 frees, 40 bytes allocated ==12345== ==12345== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1 ==12345== at 0x4C2FB0F: malloc (vg_replace_malloc.c:299) ==12345== by 0x1091A9: build_report (leak.c:4) ==12345== by 0x1091C7: main (leak.c:9) ==12345== ==12345== LEAK SUMMARY: ==12345== definitely lost: 40 bytes in 1 blocks ==12345== indirectly lost: 0 bytes in 0 blocks ==12345== possibly lost: 0 bytes in 0 blocks ==12345== still reachable: 0 bytes in 0 blocks
The report gives you the exact allocation site (leak.c:4) and the call stack that reached it, so tracking down the fix — adding a free(scores); before returning — is usually fast. "Definitely lost" means Valgrind found no remaining pointer anywhere in the program that could still reach that block; it is a real leak.
Common Valgrind Flags
Flag | Purpose |
|---|---|
| Shows details for every individual leaked block instead of just a summary count. |
| Traces uninitialized values back to where they were created (slower, but very helpful). |
| Reports all leak categories (definite, indirect, possible, still reachable). |
| Makes Valgrind return a non-zero exit code on errors, useful for CI pipelines. |