How to Use Valgrind to Detect Memory Leaks in C++
Memory leaks are one of the most annoying and hard-to-find bugs in C++. They can cause your program to crash, run slowly, or behave unpredictably. Fortunately, there is a tool that can help you find and fix memory leaks easily: Valgrind.
Valgrind is a suite of tools for debugging and profiling C++ programs. The most popular tool in the suite is Memcheck, which can detect memory errors such as:
- Using uninitialized memory
- Reading or writing past the end of an array
- Freeing memory twice
- Forgetting to free memory
- Using memory after freeing it
In this blog, I will show you how to use Valgrind Memcheck to detect memory leaks in a simple C++ program. I will also give you some tips on how to avoid memory leaks in the first place.
Preparing Your Program for Valgrind
Before you can use Valgrind, you need to compile your program with some flags that will make Valgrind’s output more useful. These flags are:
-g
: This flag tells the compiler to include debugging information in the executable file. This will allow Valgrind to show you the exact line numbers where the errors occur.-O0
: This flag tells the compiler to disable optimization. Optimization can make your code faster, but it can also make it harder for Valgrind to detect errors. For example, optimization can remove some variables or reorder some instructions, which can confuse Valgrind.
You can add these flags to your usual compilation command. For example, if your program is called a.cpp
, you can compile it with this command:
g++ -g -O0 a.cpp -o a
Running Your Program with Valgrind
To run your program with Valgrind Memcheck, you need to add some options to the valgrind
command. These options are:
--leak-check=yes
: This option tells Valgrind to perform a detailed analysis of memory leaks at the end of the program execution. It will show you how many bytes of memory are leaked, and where they were allocated.--show-leak-kinds=all
: This option tells Valgrind to show all kinds of memory leaks, not just the ones that are definitely lost. There are four kinds of memory leaks that Valgrind can detect:- Definitely lost: This means that the program has lost all pointers to a block of memory, and cannot free it anymore. This is the most serious kind of leak, and should be fixed as soon as possible.
- Indirectly lost: This means that the program has lost some pointers to a block of memory, but not all of them. However, the remaining pointers are not accessible from the root set of pointers (such as global variables or stack frames), and cannot be used to free the memory. This kind of leak is usually caused by a definitely lost block that contains pointers to other blocks.
- Possibly lost: This means that the program has lost some pointers to a block of memory, but not all of them. However, the remaining pointers are not definitely valid pointers, and may point to the middle of another block or outside the heap. This kind of leak is usually caused by pointer arithmetic or casting errors.
- Still reachable: This means that the program has not lost any pointers to a block of memory, but has not freed it before exiting. This kind of leak is usually harmless, but may indicate poor programming style or logic errors.
--track-origins=yes
: This option tells Valgrind to track the origins of uninitialized values that cause errors. Uninitialized values are values that have not been assigned a value before being used. They can cause unpredictable behavior and hard-to-reproduce bugs. By tracking their origins, Valgrind can show you where they were allocated or declared.--verbose
: This option tells Valgrind to show more information about what it is doing and what it finds. This can help you understand the error messages better and debug your program faster.--log-file
: This option tells Valgrind to write its output to a file instead of printing it on the screen. This can be useful if your program produces a lot of output or if you want to save it for later analysis.
You can add these options after the valgrind
command and before your program name and arguments. For example, if your program is called a
and takes no arguments, you can run it with this command:
valgrind --leak-check=yes --show-leak-kinds=all --track-origins=yes --verbose --log-file=valgrind.log ./a
Valgrind will run your program much slower than normal, and use a lot more memory. It will also write its output to the file valgrind.log
, which you can open and read later. The output will contain messages about memory errors and leaks that Valgrind detects. You need to read these messages carefully and understand what they mean.
Interpreting Valgrind’s Output
Here is an example C++ program, in a file called a.cpp
, with a memory error and a memory leak.
#include <iostream>
using namespace std;
void f() {
int* x = new int[10];
x[10] = 0; // problem 1: heap block overrun
} // problem 2: memory leak -- x not deleted
int main() {
f();
return 0;
}
If we run this program under Valgrind with the options we discussed before, we will get an output like this:
12345 Memcheck, a memory error detector
12345 Copyright © 2002-2017, and GNU GPL’d, by Julian Seward et al.
12345 Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
12345 Command: ./a
12345
–12345-- Valgrind options:
–12345-- --leak-check=yes
–12345-- --show-leak-kinds=all
–12345-- --track-origins=yes
–12345-- --verbose
–12345-- --log-file=valgrind.log
–12345-- Contents of /proc/version:
–12345-- Linux version 4.15.0-29-generic (buildd@lgw01-amd64-039) (gcc version 7.3.0 (Ubuntu 7.3.0-16ubuntu3)) #31-Ubuntu SMP Tue Jul 17 15:39:52 UTC 2018
–12345–
–12345-- Arch and hwcaps: AMD64, LittleEndian, amd64-cx16-lzcnt-rdtscp-sse3-avx-avx2-bmi
–12345-- Page sizes: currently 4096, max supported 4096
–12345-- Valgrind library directory: /usr/lib/valgrind
–12345-- Reading syms from /home/user/a
–12345-- Reading syms from /lib/x86_64-linux-gnu/ld-2.27.so
–12345-- Considering /lib/x86_64-linux-gnu/ld-2.27.so …
–12345-- … CRC mismatch (computed c075befa wanted 5b162bac)
–12345-- Considering /usr/lib/debug/lib/x86_64-linux-gnu/ld-2.27.so …
–12345-- … CRC is valid
–12345-- Reading syms from /usr/lib/valgrind/memcheck-amd64-linux
–12345-- Considering /usr/lib/valgrind/memcheck-amd64-linux …
–12345-- … CRC mismatch (computed 9f02f9f1 wanted a932c1bd)
–12345-- object doesn’t have a symbol table
12345 Invalid write of size 4
12345 at 0x108668: f() (a.cpp:6)
12345 by 0x10867A: main (a.cpp:11)
12345 Address 0x522d040 is 0 bytes after a block of size 40 alloc’d
12345 at 0x4C2C25F: operator new[](unsigned long) (vg_replace_malloc.c:423)
12345 by 0x10865D: f() (a.cpp:5)
12345 by 0x10867A: main (a.cpp:11)
12345
…
Things to notice:
- There is a lot of information in each error message; read it carefully.
- The
12345
is the process ID; it’s usually unimportant. - The first line (
"Invalid write..."
) tells you what kind of error it is. Here, the program wrote to some memory it should not have due to a heap block overrun. - Below the first line is a stack trace telling you where the problem occurred. Stack traces can get quite large, and be confusing, especially if you are using the C++ STL. Reading them from the bottom up can help. If the stack trace is not big enough, use the
--num-callers
option to make it bigger. - The code addresses (eg.
0x108668
) are usually unimportant, but occasionally crucial for tracking down weirder bugs. - The last line (
"Address 0x522d040 is 0 bytes after a block of size 40 alloc'd"
) tells you where the memory was allocated and how much of it was allocated. This can help you find the source of the leak.
At the end of the program execution, Valgrind will also show you a summary of memory leaks, like this:
12345
12345 HEAP SUMMARY:
12345 in use at exit: 40 bytes in 1 blocks
12345 total heap usage: 2 allocs, 1 frees, 72,744 bytes allocated
12345
12345 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
12345 at 0x4C2C25F: operator new[](unsigned long) (vg_replace_malloc.c:423)
12345 by 0x10865D: f() (a.cpp:5)
12345 by 0x10867A: main (a.cpp:11)
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
12345 suppressed: 0 bytes in 0 blocks
…
- The
HEAP SUMMARY
shows you how much memory was allocated and freed during the program execution. It also shows you how much memory was still in use at exit, which indicates memory leaks. - The
LEAK SUMMARY
shows you how many bytes and blocks of memory were leaked, and what kind of leaks they were. It also shows you where they were allocated, which can help you find the source of the leak. - The
suppressed
category shows you how many bytes and blocks of memory were suppressed by Valgrind. Suppression is a mechanism that allows you to ignore some errors or leaks that are not caused by your program or that are not important. You can use a suppression file to specify which errors or leaks to suppress. For more details, see the Valgrind User Manual.
Fixing Memory Leaks
Once you have identified the source of the memory leaks, you need to fix them by modifying your code. There are two common ways to fix memory leaks in C++:
- Use
delete
ordelete[]
to free the memory that was allocated with new or new[]. For example, in our example program, we can fix the memory leak by adding this line at the end of function f():
delete[] x;
- Use smart pointers instead of raw pointers to manage memory automatically. Smart pointers are classes that wrap around raw pointers and provide features such as reference counting, ownership transfer, and custom deleters. They can free the memory when they go out of scope or when they are no longer needed. There are several types of smart pointers in C++, such as
std::unique_ptr
,std::shared_ptr
, andstd::weak_ptr
. For example, in our example program, we can fix the memory leak by replacing this line:
int* x = new int[10];
with this line
std::unique_ptr<int[]> x(new int[10]);
Avoiding Memory Leaks
The best way to deal with memory leaks is to avoid them in the first place. Here are some tips on how to avoid memory leaks in C++:
- Prefer smart pointers over raw pointers whenever possible. Smart pointers can make your code safer, cleaner, and easier to maintain. They can also prevent memory leaks by freeing the memory automatically when they go out of scope or when they are no longer needed.
- Use the RAII (Resource Acquisition Is Initialization) idiom to manage resources. RAII is a technique that binds the lifetime of a resource (such as memory, file, socket, etc.) to the lifetime of an object. When the object is created, it acquires the resource. When the object is destroyed, it releases the resource. This way, you don’t have to worry about forgetting to free the resource or handling exceptions.
- Avoid using
new
anddelete
directly. Instead, use standard containers (such asstd::vector
,std::string
,std::map
, etc.) or factory functions (such asstd::make_unique
,std::make_shared
, etc.) to manage memory for you. These classes and functions can handle memory allocation and deallocation for you, and avoid common errors such as mismatchednew
anddelete
, memory leaks, or dangling pointers. - Use static analysis tools (such as Clang-Tidy, Cppcheck, etc.) or code review tools (such as Code Review Assistant, Codacy, etc.) to check your code for potential memory leaks or other issues. These tools can help you find and fix bugs before they cause problems at runtime.
Conclusion
In this blog, I showed you how to use Valgrind Memcheck to detect and fix memory leaks in C++ programs. I also gave you some tips on how to avoid memory leaks in the first place. I hope you found this blog useful and learned something new.
If you have any questions or feedback, please leave a comment below. Thank you for reading!