How Do I Debug Memory Errors with Valgrind?
Memory leaks, invalid reads, use-after-free — Valgrind finds bugs you can't see
After this lesson, you will be able to:
- Compile with
-gand run undervalgrind --leak-check=fullto detect memory errors - Interpret Valgrind output for leaks, invalid reads/writes, use-after-free, and double-free
- Map a Valgrind error to the specific source code line
- Identify clean Valgrind output (0 errors, all blocks freed)
The Bugs You Can’t See
Your program compiles. It runs. It produces the right output. Everything looks fine.
But it’s leaking memory. Every time the user adds a record, your program consumes more RAM that it never gives back. After running for an hour, it’s using 2 GB. After a day, it crashes.
Memory bugs are invisible at the surface. The program “works” — until it doesn’t. That’s where Valgrind comes in. It watches every memory access your program makes and reports anything suspicious.
Using Valgrind
Basic Usage
First, compile with debug symbols (-g):
gcc -Wall -g -o myprogram myprogram.c
Then run through Valgrind:
valgrind --leak-check=full ./myprogram
Valgrind runs your program normally but instruments every memory operation. It’s slower (10-20x), but it catches bugs that no other tool can.
Reading Valgrind Output
Clean output (no errors):
==12345== HEAP SUMMARY:
==12345== in use at exit: 0 bytes in 0 blocks
==12345== total heap usage: 3 allocs, 3 frees, 1,200 bytes allocated
==12345==
==12345== All heap blocks were freed -- no leaks are possible
==12345==
==12345== ERROR SUMMARY: 0 errors from 0 contexts
This is what you want: 0 bytes in use at exit, all blocks freed, 0 errors.
Memory Leak
int *arr = calloc(100, sizeof(int));
// ... forgot to free(arr) ...
Valgrind reports:
==12345== LEAK SUMMARY:
==12345== definitely lost: 400 bytes in 1 blocks
Fix: Add free(arr) before the program exits.
Invalid Read
int *arr = calloc(5, sizeof(int));
printf("%d\n", arr[10]); // Reading past the end
free(arr);
==12345== Invalid read of size 4
==12345== at 0x40069C: main (buggy.c:5)
==12345== Address 0x5205068 is 20 bytes after a block of size 20 alloc'd
Valgrind tells you exactly which line caused the problem and how far past the allocation you read.
Invalid read of size 4 ... Address is 20 bytes after a block of size 20 alloc'd. What happened?<= instead of < in a loop. Option A would say "inside a block ... free'd." Option B would appear in the LEAK SUMMARY, not as an invalid read.
Use After Free
int *arr = calloc(5, sizeof(int));
free(arr);
arr[0] = 42; // Writing to freed memory!
==12345== Invalid write of size 4
==12345== at 0x40069C: main (buggy.c:4)
==12345== Address 0x5205040 is 0 bytes inside a block of size 20 free'd
Fix: Set arr = NULL after free(arr) — then this would be a clean segfault instead of silent corruption.
Uninitialized Value
int *arr = malloc(5 * sizeof(int)); // Not calloc — uninitialized!
if (arr[0] > 10) { ... } // Reading garbage
==12345== Conditional jump or move depends on uninitialised value(s)
==12345== at 0x40069C: main (buggy.c:3)
Fix: Use calloc instead of malloc, or initialize values before reading them.
Double Free
int *arr = calloc(5, sizeof(int));
free(arr);
free(arr); // Freeing the same block twice!
==12345== Invalid free() / delete / delete[] / realloc()
==12345== at 0x4C2EDEB: free (vg_replace_malloc.c:530)
==12345== by 0x40069C: main (buggy.c:4)
==12345== Address 0x5205040 is 0 bytes inside a block of size 20 free'd
Fix: Set arr = NULL after free(arr). Calling free(NULL) is safe and does nothing — it’s a no-op by the C standard.
Deep dive: Valgrind key phrases cheat sheet
When reading Valgrind output, look for these phrases:
| Valgrind says… | What it means | Common cause |
|---|---|---|
Invalid read of size N |
Reading memory you don’t own | Array out of bounds, use-after-free |
Invalid write of size N |
Writing memory you don’t own | Buffer overflow, use-after-free |
Conditional jump depends on uninitialised value |
Using uninitialized memory in an if or loop |
malloc without initializing, reading before writing |
Invalid free() |
Freeing memory that was already freed or never allocated | Double free, freeing stack memory |
definitely lost: N bytes |
Memory leaked — no pointer to it exists at exit | Forgot free(), overwrote pointer before freeing |
indirectly lost: N bytes |
Memory reachable only through a leaked pointer | Leaked a struct that contains pointers to other allocations |
still reachable: N bytes |
Memory still pointed to at exit but not freed | Usually not a bug — but clean it up anyway |
All heap blocks were freed |
No leaks at all | This is the goal |
The number after == (like ==12345==) is just the process ID — ignore it. Focus on the error description and the source file/line number (only available if you compiled with -g).
The Valgrind Checklist
Before submitting any lab with dynamic memory:
gcc -Wall -g -o program program.c
valgrind --leak-check=full ./program
Look for:
All heap blocks were freed -- no leaks are possibleERROR SUMMARY: 0 errors from 0 contexts
Key Insight: Valgrind is your best friend for memory bugs. Java would never let you make these mistakes (garbage collection, bounds checking, no pointer arithmetic). C lets you make all of them — and Valgrind catches them after the fact. Run it on every program that uses
calloc,malloc, orfree.
definitely lost: 800 bytes in 2 blocks. What should you do?free() calls. Option A is wrong — correct output doesn't mean correct memory management. In a long-running program, these leaks accumulate. Option D is wrong — calloc vs malloc has nothing to do with freeing.
Why does this matter?
In industry, memory leaks kill servers. A web server leaking 800 bytes per request will consume gigabytes in hours. Valgrind is the standard tool for catching these bugs before deployment. Getting clean Valgrind output is a grading requirement for labs with dynamic memory — and a professional skill that transfers directly to C/C++ jobs.
From Java: Java throws
ArrayIndexOutOfBoundsException,NullPointerException, and handles memory automatically. C gives you none of that. Valgrind is the closest thing C has to Java’s runtime safety checks — but you have to run it yourself, and it only catches bugs that actually execute during your test.
Quick Check: What does "definitely lost: 400 bytes in 1 blocks" mean?
You allocated 400 bytes (probably calloc(100, sizeof(int))) and never freed them. The memory is leaked — no pointer to it exists at program exit, so it can never be freed. Add a free() call.
Quick Check: Why compile with -g for Valgrind?
The -g flag includes debug information (source file names and line numbers) in the executable. Without it, Valgrind can still detect errors but can’t tell you which line of source code caused them — you’d only see memory addresses.
Quick Check: Can Valgrind catch a bug that doesn't happen during your test run?
No. Valgrind only catches errors that actually occur during the specific run you test. If your test doesn’t trigger a particular code path (e.g., the realloc branch), Valgrind can’t check it. That’s why thorough test cases matter.
Trust but Verify
Valgrind doesn’t prevent bugs — it detects them. The real fix is writing correct code: matching every calloc with free, checking every allocation for NULL, and never accessing out-of-bounds memory. But Valgrind catches the mistakes you miss.
Next: putting it all together with a complete program that uses dynamic memory, menus, and input validation. This is the capstone for Series 3.
Big Picture: Valgrind is the safety net that makes manual memory management survivable. Without it, C programmers would be debugging invisible memory corruption by staring at hex dumps. With it, you get precise error reports that point to the exact line of code. Every lab with dynamic memory from here on requires clean Valgrind output — treat it as part of the compilation step.