A real Akuna new-grad candidate cleared four technical rounds and got cut on the C++ debugging round — because they normally coded in Java and reading broken C++ under time pressure is a distinct, trainable skill. This module trains it directly. Do the drills until spotting these bugs is reflexive.
The undefined behavior catalog
UB means the standard imposes no requirements — the program may crash, corrupt data, or appear to work until it doesn't. Memorize these categories; they are the raw material of every debugging round.
1. Use-after-free using memory after delete
2. Double free delete the same pointer twice
3. Dangling pointer/ref pointer to a destroyed object (e.g. returned local)
4. Out-of-bounds access arr[n], vector[i] past the end
5. Uninitialized read reading a variable before it is set
6. Null dereference *p when p is nullptr
7. Signed integer overflow INT_MAX + 1
8. Iterator invalidation using an iterator after the container changed
9. Data race unsynchronized shared read/write across threads
10. Returning ref to temporary const std::string& r = makeTemp();
11. Missing virtual destructor delete via base pointer, derived not destroyed
12. Object slicing copying a derived into a base by valueReading broken code: a method
When handed a file to debug, don't read top to bottom. Scan for the high-probability bug sites in this order:
- Every
new/deleteand pointer. Is eachnewmatched exactly once? Any use afterdelete? Any returned local address? - Every array/container index and loop bound. Off-by-one?
<=where it should be<? Unsigned loop counter going below zero? - Every variable declaration. Initialized before use?
- Every class with a destructor. Does it obey the Rule of Five? Base class destructor virtual?
- Every loop that erases from a container. Iterator invalidated mid-loop?
- Anything touching shared state across threads. Protected by a lock or atomic?
Worked examples
std::vector<int> v = {1,2,3};
for (size_t i = 0; i <= v.size(); ++i) // BUG: <= reads v[3], out of bounds
std::cout << v[i];class Base { public: ~Base() {} }; // BUG: non-virtual destructor
class Derived : public Base { int* p_ = new int[100]; public: ~Derived(){ delete[] p_; } };
Base* b = new Derived();
delete b; // only ~Base runs -> Derived's destructor skipped -> leakint sum = 0; // (fine)
int values[3];
for (int i = 0; i < 3; ++i) sum += values[i]; // BUG: values uninitializedThe tools
Compiler warnings first
Before any tool, recompile with -Wall -Wextra -Wpedantic. A huge fraction of bugs are already flagged there for free.
Sanitizers — your fastest bug-finders
# AddressSanitizer: catches use-after-free, out-of-bounds, double-free
g++ -std=c++17 -g -fsanitize=address -fno-omit-frame-pointer bug.cpp -o bug
./bug # prints the exact line and allocation stack on the first violation
# UndefinedBehaviorSanitizer: catches signed overflow, bad casts, more
g++ -std=c++17 -g -fsanitize=undefined bug.cpp -o bug
# ThreadSanitizer: catches data races
g++ -std=c++17 -g -fsanitize=thread race.cpp -o raceMention that you build tests under AddressSanitizer and UBSan. It signals you've been burned by memory bugs and know the professional tooling. At a trading firm, ASan/UBSan/TSan in CI is standard practice.
GDB — stepping through
g++ -std=c++17 -g bug.cpp -o bug # -g adds debug symbols
gdb ./bug
(gdb) break main # set a breakpoint
(gdb) run # start
(gdb) next # step over one line
(gdb) step # step into a call
(gdb) print x # inspect a variable
(gdb) backtrace # the call stack (great after a crash)Valgrind — leak and memory-error detection
valgrind --leak-check=full ./bug # reports leaks and invalid accesses with line numbersExercise 11.1 — The debugging gauntlet
This is the most important exercise in the course for your interviews.
- Take your own
Bufferclass from Module 05. Introduce each of these bugs one at a time and find it with the right tool: a leak (Valgrind), a use-after-free (ASan), a signed overflow (UBSan). - Write a 60–100 line program with 5 planted bugs from the catalog. Leave it a day. Come back and time yourself finding all 5. Target: under 15 minutes. Repeat weekly with new bugs.
- Take the base/derived example above, run it under Valgrind to see the leak, then add
virtual ~Base()and confirm the leak disappears. - Reproduce a data race, catch it with ThreadSanitizer, then fix it with a mutex and confirm TSan goes quiet.
Takeaways
- Know the UB catalog cold — it's the raw material of every debugging round.
- Don't read broken code linearly; scan the high-probability sites (pointers, bounds, init, Rule of Five, iterator loops, shared state).
- Recompile with full warnings before anything else.
- ASan/UBSan/TSan find memory bugs, UB, and races with exact locations — learn the flags.
- GDB for stepping and backtraces; Valgrind for leaks. Practice the debugging gauntlet weekly.
Loading starter code…
Checkpoint · Debugging & UB
6 questions · pass at 70%
for (size_t i = 0; i <= v.size(); ++i) use(v[i]);. What is the bug?Finished Module 11?
Pass the quiz to complete it automatically.