Your program runs and produces correct output. Then you ship it and it crashes in production after 3 hours. The cause: a memory leak that slowly exhausts RAM, or a use-after-free that corrupts the heap. Valgrind would have caught this in development. Here is how to use it.

Running Valgrind

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

Always compile with -g -O0 before running Valgrind. Without debug symbols, error messages show hex addresses instead of file and line numbers. Without -O0, inlined functions and register-allocated variables confuse the stack trace.

Memory Leak

#include <stdlib.h>

void leaky_function() {
    int *data = malloc(100 * sizeof(int));
    /* forgot to free(data) */
}

int main() {
    leaky_function();
    return 0;
}
==12345== HEAP SUMMARY:
==12345==     in use at exit: 400 bytes in 1 blocks
==12345==   total heap usage: 1 allocs, 0 frees, 400 bytes allocated
==12345==
==12345== LEAK SUMMARY:
==12345==    definitely lost: 400 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

==12345== 400 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345==    at 0x...: malloc (vg_replace_malloc.c:...)
==12345==    at 0x...: leaky_function (leak.c:4)
==12345==    at 0x...: main (leak.c:9)

Valgrind identifies the allocation site, not just the total leaked bytes. “Definitely lost” means there is no remaining pointer to the allocation — it is truly unrecoverable. “Still reachable” means the pointer exists but was never freed — less urgent but still a bug.

Invalid Read — Use After Free

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *p = malloc(sizeof(int));
    *p = 42;
    free(p);
    printf("%dn", *p);   /* use-after-free */
    return 0;
}
==12346== Invalid read of size 4
==12346==    at 0x...: main (uaf.c:7)
==12346==  Address 0x... is 0 bytes inside a block of size 4 free'd
==12346==    at 0x...: free (vg_replace_malloc.c:...)
==12346==    at 0x...: main (uaf.c:6)

Valgrind pinpoints the read and the earlier free. This is the information you need to find the bug — the line of the read, and the call stack of the free that made it invalid.

Invalid Write — Buffer Overflow

#include <stdlib.h>

int main() {
    int *arr = malloc(5 * sizeof(int));
    arr[5] = 999;   /* one past the end */
    free(arr);
    return 0;
}
==12347== Invalid write of size 4
==12347==    at 0x...: main (overflow.c:5)
==12347==  Address 0x... is 0 bytes after a block of size 20 alloc'd

Uninitialized Memory

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *arr = malloc(10 * sizeof(int));
    /* forgot to initialize */
    printf("%dn", arr[3]);   /* reading uninitialized heap memory */
    free(arr);
    return 0;
}
==12348== Use of uninitialised value of size 4
==12348==    at 0x...: main (uninit.c:6)

Valgrind tracks which bytes have been initialized. Reading an uninitialized byte — even if it has a plausible-looking value at runtime — is flagged. This catches bugs that only manifest with certain heap layouts or compilers.

Valgrind Flags Reference

FlagWhat it does
--leak-check=fullShow details for every leaked block
--show-leak-kinds=allInclude “still reachable” leaks
--track-origins=yesShow where uninitialized values came from (slower)
--error-exitcode=1Return non-zero exit code if errors found — useful in CI
--gen-suppressions=allGenerate suppression patterns for known false positives

Valgrind vs AddressSanitizer

Both tools catch memory bugs, but they work differently:

ValgrindAddressSanitizer (ASan)
Slowdown10-30×
Requires recompileNoYes (-fsanitize=address)
Stack overflow detectionLimitedYes
Uninit memoryYes (Memcheck)Partial (MSan)
Works on existing binaryYesNo

Use ASan for your regular development cycle — it is fast enough to leave enabled. Reserve Valgrind for uninit memory bugs (use --track-origins=yes) and when you cannot recompile. Our guide on malloc, calloc, and realloc covers the allocation patterns that Valgrind most often flags. Use the gcc compiler online to test sanitizer-compatible compilation flags before setting up a local Valgrind run.

TL;DR

  • Compile with -g -O0 for readable Valgrind output
  • valgrind --leak-check=full --show-leak-kinds=all ./program is the standard invocation
  • “Definitely lost” = truly leaked; “Still reachable” = pointer exists but never freed
  • Use-after-free and invalid reads show the read site AND the free site — both are needed to fix the bug
  • Add --track-origins=yes for uninit memory — it slows Valgrind further but names the allocation
  • Use --error-exitcode=1 in CI to fail builds with memory errors
  • For fast daily use, prefer AddressSanitizer; use Valgrind for thorough checks and uninit detection