Debugging C++ and Undefined Behavior
Classify compile, link, runtime, logic, and undefined-behavior failures, then investigate them with warnings, sanitizers, a debugger, and small test cases.
Before this lesson
Classify compile link runtime logic and undefined behavior
Use breakpoints stepping watches and call stacks
Explain why warnings and sanitizers complement tests
The short answer
Reproduce the smallest failing case, identify the failure stage, enable strong warnings, and inspect actual state. Undefined behavior means the language no longer constrains the result, so an apparently successful run is not proof.
Classify the failure stage
A compiler error prevents translation, a linker error prevents the final executable, a runtime failure interrupts execution, and a logic error produces the wrong result. C++ also has undefined behavior: the program violates a rule for which the standard imposes no required outcome.
Classification determines the first evidence to inspect. Read compiler and linker messages exactly. For a crash, capture the input, exception or signal, and call stack. For wrong output, compare state against a hand-worked example.
| Failure | Evidence | First move |
|---|---|---|
| Compile | Diagnostic and source location | Fix the earliest useful message |
| Link | Undefined or duplicate symbol | Compare declarations, definitions, and build inputs |
| Runtime | Crash or sanitizer report | Inspect the call stack and state |
| Logic | Wrong output | Trace a small expected example |
| Undefined behavior | Unstable or surprising behavior | Find the violated rule, not a cosmetic workaround |
Undefined behavior breaks prediction
Reading outside a container, using an object after its lifetime ends, signed integer overflow, and reading an uninitialized fundamental value are common sources of undefined behavior. The compiler may assume these cases never occur while optimizing valid programs.
Do not reason from one observed run. Correct the violated precondition or lifetime. Standard containers, bounds-aware access during investigation, RAII, and strong types reduce the surface area but do not remove the need for contracts.
Debug with evidence
Set a breakpoint just before the suspected decision or access. Step Over executes a call without entering it; Step Into follows the call; Step Out returns to its caller. Watches track expressions, and the call stack explains how execution reached the current line.
Temporary stream output can also expose state in constrained environments. Record only values tied to a hypothesis, then remove the noise. Change one cause at a time and rerun the original failing input plus nearby boundaries.
for (std::size_t index{}; index <= values.size(); ++index)
{
std::cerr << "index=" << index
<< ", size=" << values.size() << '\n';
std::cout << values[index] << '\n'; // invalid when index == size
}Compile with diagnostics enabled
Strong warning levels catch suspicious conversions, shadowed names, missing returns, and other defects before runtime. Treat new warnings as work to understand, not messages to suppress reflexively. Different compilers find different issues, so portable projects benefit from more than one toolchain in continuous integration.
Sanitizers complement warnings by instrumenting an executable. They are not proofs of correctness: they detect exercised failures. Combine them with boundary tests, debugger inspection, code review, and clear ownership.
Quick knowledge check
Answer before you reveal.
01Can undefined behavior appear to work?
Yes. The program may seem correct for one build or input and fail after an unrelated change because no portable behavior is guaranteed.
02Why inspect the first relevant compiler diagnostic?
Later messages may be cascading consequences of the first error.
Exercise
Practice challenge
Diagnose a loop that reads values[values.size()]. Reproduce it, predict the first invalid position, inspect the index and size, then correct the stopping condition and test empty one-item and three-item vectors.
Requirements
- You identify the invalid index before changing code
- The fix derives from the valid half-open range
- All three boundary vectors behave correctly
Optional extension: Run the failing version locally with AddressSanitizer enabled.
Open in C++ compilerLesson checkpoint
One small step locks it in
Mark this lesson complete, then keep the momentum going.
Clear up the details
Frequently asked questions
Are warnings errors?
They do not always stop a build, but many reveal real portability, lifetime, conversion, or logic risks and deserve investigation.
What does a sanitizer do?
It instruments a build to detect classes of runtime errors such as invalid memory access or undefined operations with useful diagnostics.
Is debugging the same as testing?
Testing looks for evidence that behavior meets requirements; debugging investigates a known mismatch. Each makes the other more effective.