C++ Debugging with GDB & Valgrind: Find & Fix Bugs Guide 2026
Table of Contents
Why Debugging Skills Matter
Writing C++ code that compiles is one thing. Writing code that works correctly is another entirely. C++ gives you direct memory access, pointer arithmetic, and manual resource management — powerful features that produce subtle, hard-to-find bugs when used incorrectly. Memory leaks, buffer overflows, dangling pointers, and data races don’t always crash immediately — they corrupt data silently and fail unpredictably.
Professional C++ developers spend significant time debugging. The tools in this lesson — GDB for interactive debugging, Valgrind for memory analysis, and sanitizers for catching undefined behavior — are non-negotiable skills. They turn impossible “it works on my machine” bugs into clear, reproducible, fixable problems.
Compiling for Debugging
# Always compile with -g for debug symbols
g++ -g -O0 main.cpp -o myapp
# -g : Include debug information (line numbers, variable names)
# -O0 : No optimization (code matches source exactly)
# -Wall -Wextra : Enable warnings (catch bugs at compile time)
# With CMake:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build
# Debug builds are bigger and slower but debuggable
# Release builds (-O2 or -O3) optimize away variables and reorder code
GDB Basics
# Start GDB with your program
gdb ./myapp
# GDB commands:
(gdb) run # Run the program
(gdb) run arg1 arg2 # Run with arguments
(gdb) quit # Exit GDB
(gdb) help # Show help
(gdb) help breakpoints # Help on specific topic
# Run with input redirection
(gdb) run < input.txt
# Attach to running process
gdb -p 12345 # Attach to PID 12345
Breakpoints
# Set breakpoints (pause execution at specific points)
(gdb) break main # Break at function main()
(gdb) break main.cpp:42 # Break at line 42 of main.cpp
(gdb) break MyClass::method # Break at class method
(gdb) break 42 if x > 10 # Conditional breakpoint
# List, enable, disable, delete breakpoints
(gdb) info breakpoints # List all breakpoints
(gdb) disable 2 # Disable breakpoint #2
(gdb) enable 2 # Re-enable it
(gdb) delete 2 # Remove breakpoint #2
(gdb) delete # Remove all breakpoints
# Temporary breakpoint (auto-deletes after hit)
(gdb) tbreak main.cpp:50
Stepping Through Code
# After hitting a breakpoint:
(gdb) next # Execute current line, step OVER function calls
(gdb) step # Execute current line, step INTO function calls
(gdb) finish # Run until current function returns
(gdb) continue # Resume execution until next breakpoint
(gdb) until 60 # Run until line 60
# next vs step:
# If current line is: result = compute(x);
# next → executes compute() entirely, stops at next line
# step → enters compute(), stops at its first line
# Reverse debugging (if supported)
(gdb) reverse-step
(gdb) reverse-next
(gdb) reverse-continue
Inspecting Variables and Memory
# Print variables
(gdb) print x # Print variable x
(gdb) print *ptr # Dereference pointer
(gdb) print array[5] # Array element
(gdb) print obj.member # Object member
(gdb) print sizeof(x) # Size of variable
# Print expressions
(gdb) print x + y * 2
(gdb) print (double)x / y
# Print with format
(gdb) print/x val # Hexadecimal
(gdb) print/t val # Binary
(gdb) print/d val # Decimal
# Display: auto-print at every stop
(gdb) display x # Show x after every step
(gdb) undisplay 1 # Stop showing display #1
# Examine memory
(gdb) x/10d ptr # 10 decimal ints at ptr
(gdb) x/20xb ptr # 20 hex bytes
(gdb) x/s str_ptr # String at pointer
# Stack trace
(gdb) backtrace # Show call stack (bt for short)
(gdb) frame 3 # Switch to frame #3
(gdb) info locals # All local variables in current frame
(gdb) info args # Function arguments
Watchpoints
# Watch: break when a variable changes
(gdb) watch x # Break when x is modified
(gdb) watch *ptr # Break when value at ptr changes
(gdb) rwatch x # Break when x is READ
(gdb) awatch x # Break on read OR write
# Watch with condition
(gdb) watch x if x > 100
# Watchpoints are slow (hardware-limited to ~4) but invaluable
# for finding "who changed this variable?"
Debugging Crashes
# When program crashes (segfault), GDB stops at the crash point
(gdb) run
# Program received signal SIGSEGV, Segmentation fault.
(gdb) backtrace # See where it crashed
(gdb) frame 0 # Go to crash frame
(gdb) print ptr # Often the culprit is a null/dangling pointer
(gdb) info registers # CPU register state
# Core dumps: debug a crash after the fact
# Enable core dumps:
ulimit -c unlimited
./myapp # Crashes, creates core file
# Debug the core dump:
gdb ./myapp core
(gdb) backtrace # See crash location
Valgrind — Memory Error Detection
# Valgrind runs your program in a virtual CPU and tracks every
# memory access. It finds bugs that don't crash.
# Basic usage:
valgrind ./myapp
# Detailed leak check:
valgrind --leak-check=full --show-leak-kinds=all ./myapp
# Track origins of uninitialized values:
valgrind --track-origins=yes ./myapp
// What Valgrind catches:
// 1. Memory leaks
void leak() {
int* p = new int[100];
// Never deleted — Valgrind reports: "definitely lost: 400 bytes"
}
// 2. Use after free
void use_after_free() {
int* p = new int(42);
delete p;
*p = 10; // Valgrind: "Invalid write of size 4"
}
// 3. Buffer overflow
void overflow() {
int arr[10];
arr[10] = 42; // Valgrind: "Invalid write of size 4"
}
// 4. Uninitialized reads
void uninitialized() {
int x;
if (x > 0) {} // Valgrind: "Conditional jump depends on uninitialised value"
}
// 5. Double free
void double_free() {
int* p = new int(42);
delete p;
delete p; // Valgrind: "Invalid free"
}
Address Sanitizer (ASan)
# Compile with ASan (faster than Valgrind, ~2x slowdown vs 20x)
g++ -g -fsanitize=address -fno-omit-frame-pointer main.cpp -o myapp
./myapp
# ASan catches:
# - Buffer overflows (stack, heap, global)
# - Use after free
# - Use after return (with flag)
# - Double free
# - Memory leaks (with ASAN_OPTIONS=detect_leaks=1)
# Environment options:
ASAN_OPTIONS=detect_leaks=1:halt_on_error=0 ./myapp
Undefined Behavior Sanitizer (UBSan)
# Compile with UBSan
g++ -g -fsanitize=undefined main.cpp -o myapp
./myapp
# Combine with ASan:
g++ -g -fsanitize=address,undefined main.cpp -o myapp
// UBSan catches:
int x = INT_MAX;
x + 1; // Signed integer overflow
int arr[10];
arr[15]; // Out of bounds
int* p = nullptr;
*p; // Null pointer dereference
int x = 1;
x << 33; // Shift by too much
double d = 1e308;
(int)d; // Float-to-int overflow
Thread Sanitizer (TSan)
# Compile with TSan (cannot combine with ASan)
g++ -g -fsanitize=thread main.cpp -o myapp -pthread
./myapp
# TSan detects data races in multithreaded code
# Reports: which threads, which variable, read vs write
Debugging Techniques
// 1. Assert: catch bugs early
#include <cassert>
void process(int* ptr, int size) {
assert(ptr != nullptr && "Null pointer passed to process");
assert(size > 0 && "Size must be positive");
// Asserts are removed in release builds (NDEBUG defined)
}
// 2. Static assert: catch bugs at compile time
static_assert(sizeof(int) == 4, "This code requires 32-bit ints");
// 3. Debug logging
#ifdef DEBUG_BUILD
#define LOG(msg) std::cerr << __FILE__ << ":" << __LINE__ << " " << msg << "
"
#else
#define LOG(msg)
#endif
// 4. Narrow down with binary search
// Comment out half the code. Does the bug persist?
// If yes, it's in the remaining half. Repeat.
// 5. Rubber duck debugging
// Explain the code line-by-line to someone (or something).
// You'll often find the bug while explaining.
IDE Debugging
Most developers use IDE debuggers rather than command-line GDB. VS Code with the C/C++ extension provides a graphical debugger that uses GDB or LLDB underneath. CLion has an integrated debugger. Visual Studio on Windows has the best-in-class MSVC debugger. All of them support breakpoints, variable inspection, call stacks, and memory views — same concepts as GDB, friendlier interface.
To set up VS Code debugging, create a .vscode/launch.json file pointing to your debug build. Set breakpoints by clicking the gutter. Press F5 to start debugging. The debug console accepts GDB commands for advanced operations.
Practice Exercises
Exercise 1: Write a program with a deliberate memory leak, buffer overflow, and use-after-free. Run it through Valgrind and ASan. Compare the error reports.
Exercise 2: Create a program with a subtle bug (e.g., off-by-one in a loop, uninitialized variable used in a condition). Use GDB to step through and find the bug.
Exercise 3: Write a multithreaded program with a data race. Compile with Thread Sanitizer and fix the race it detects.
Exercise 4: Set up a CMake project with a sanitizer preset. Add ASan and UBSan build options that can be enabled via a CMake flag.
Debugging is a skill that separates professional developers from beginners. The tools are free and available on every platform — there's no excuse not to use them. Combined with unit testing (next lesson), debugging tools ensure your code works correctly and stays correct as it evolves.