A self-taught C++ learner often meets a confusing bug: the program compiles and appears to work, but fails with another input or on another machine. Warnings flag suspicious code before execution, while AddressSanitizer and UndefinedBehaviorSanitizer expose runtime failures on paths your tests execute. A repeatable compile, run, fix and rerun safety net helps expose failures, but no tool can prove that a program is bug-free.
Warnings, AddressSanitizer and UBSan catch different failures
Think in three layers. Compiler warnings are static clues, such as an uninitialised value or a lossy conversion. AddressSanitizer, or ASan, instruments executed code to catch memory errors. UndefinedBehaviorSanitizer, or UBSan, instruments operations such as signed integer overflow that have undefined behaviour in C++.
clang++ -std=c++20 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -O0 -g warnings.cpp -o warnings
clang++ -std=c++20 -Wall -Wextra -Wpedantic -O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer app.cpp -o app-sanitized-std=c++20 selects the language version. -Wall, -Wextra and -Wpedantic enable useful diagnostic groups; -Wconversion checks risky conversions and -Wshadow catches hidden names. -O0 disables optimisation for the warning build, while -O1 lightly optimises the sanitizer build. -g adds debug information, -fsanitize enables ASan and UBSan, -fno-omit-frame-pointer preserves clearer call stacks, and -o names the executable. Start without -Werror. Add it only after the baseline is clean. These commands use clang++; availability and wording vary across compilers and platforms. If the syntax is unfamiliar, follow the C++ Tutorial: Complete Learning Path in 12 Weeks, then choose focused practice from the Coding & Skill Development Courses page.
Broken program 1: let warnings expose two silent assumptions
Before compiling warnings.cpp, predict whether rounded becomes 7 or 8, and whether the if condition is safe.
#include <iostream>
int main() {
int attempts;
double average = 7.8;
int rounded = average;
if (attempts > 0) {
std::cout << rounded << '\n';
}
}Compile it with the warning command. In the Apple Clang 21.0.0 verification, the stable findings were implicit conversion turns floating-point number into integer for average to rounded, and variable 'attempts' is uninitialized when used here. Your compiler may use different wording or line numbers.
Converting 7.8 deliberately to int truncates the fractional part, so the result is 7, not 8. Reading attempts before initialisation is undefined behaviour, so guessing its value is invalid. Make both decisions explicit:
int attempts = 0;
double average = 7.8;
int rounded = static_cast<int>(average);The cast documents truncation; a different business rule could use a rounding function. Recompile with the same command until it emits zero warnings.
Broken program 2: ASan catches a use-after-free
This program allocates three live elements, 10, 20 and 30. delete[] then ends their lifetime. scores still contains an address, but it is now a dangling pointer, and scores[1] attempts to read the former value 20.
int main() {
int* scores = new int[3]{10, 20, 30};
delete[] scores;
return scores[1];
}Compile and run it with ASan:
clang++ -std=c++20 -Wall -Wextra -Wpedantic -O1 -g -fsanitize=address -fno-omit-frame-pointer use_after_free.cpp -o use-after-free
./use-after-freeThe verified report identifies AddressSanitizer: heap-use-after-free, a READ of size 4, and a location 4 bytes inside of 12-byte region. These values describe the verified platform, where each int occupies 4 bytes. Raw addresses, stack frames and byte counts can differ when the platform represents int differently, so preserve the bug class and allocation story instead of copying a hexadecimal address.
![Memory diagram of the scores array: live after new[], freed after delete[], then the scores[1] read flagged as heap-use-after-free.](https://cdn.knowledgegate.ai/blog-assets/blog_asset_1784643572399_ju7onx.jpg)
Remove manual ownership here:
#include <vector>
int main() {
std::vector<int> scores{10, 20, 30};
return scores[1] == 20 ? 0 : 1;
}RAII lets the vector release its own storage, removing the new[] and delete[] pairing.
Broken program 3: UBSan stops signed integer overflow
Make the arithmetic visible before running this program. 1,000,000,000 * 3 should equal 3,000,000,000. That is 852,516,353 above the common 32-bit signed int maximum of 2,147,483,647.
int main() {
int units = 1000000000;
int batches = 3;
int total = units * batches;
return total == 0;
}Disable recovery so the first detected undefined operation ends the run:
clang++ -std=c++20 -Wall -Wextra -Wpedantic -O1 -g -fsanitize=undefined -fno-sanitize-recover=undefined overflow.cpp -o overflow
./overflowThe verified diagnostic is signed integer overflow: 1000000000 * 3 cannot be represented in type 'int'. The correct conclusion is undefined behaviour, not a guaranteed wrapped result.

Use a type chosen for the real input range:
#include <cstdint>
int main() {
std::int64_t units = 1000000000;
std::int64_t batches = 3;
std::int64_t total = units * batches;
return total == 3000000000LL ? 0 : 1;
}The expected total is exactly 3,000,000,000. A wider type is a valid fix only after checking the largest and smallest real inputs.
Turn the tools into a repeatable debugging loop
Use the same loop on every change:
Compile the strict warning build and fix every warning you understand.
Compile an ASan and UBSan build, keeping sanitizer and release builds separate.
Run the whole test suite, fix the first report, then restart the loop because one failure can hide another.
Path coverage matters. For a container whose intended size is 3, test sizes 0, 1 and 3. At size 3, access valid indices 0 and 2. Put an index 3 access only in an isolated negative test that is meant to make ASan fail. Unexecuted paths remain unchecked.
When a class owns memory or another resource, its constructors and destructor define when that resource becomes live and when it must be released. The dangling pointer violates that lifetime. Object Oriented Technology Explained develops this ownership context without changing the central lesson: prefer designs that make valid lifetimes automatic.
Common traps that weaken the safety net
-Wall is a useful baseline, not literally every warning. -Wextra adds checks outside that baseline, -Wpedantic highlights non-standard language use, -Wconversion exposes value-changing conversions, and -Wshadow finds declarations that hide earlier names. Global warning suppression discards evidence along with noise. On a noisy inherited project, triage the warning debt before turning on -Werror.
A clean sanitizer run is not proof of correctness. ASan cannot tell you that a discount formula is logically wrong, and neither sanitizer can report a bad branch that no test executes. When saving a report, retain the bug class, operation size, source line and allocation or free story. Treat machine-specific addresses as temporary details.
How coding tests and interviews expose these bug classes
The warning example is an output-prediction trap. If code reads an uninitialised local variable, the defensible answer is undefined behaviour, not a guessed number. Apply the same reasoning to signed int overflow: C++ does not promise a particular wraparound result.
The ASan program also works as a code-review prompt. Identify the dangling pointer, explain why access after delete[] is invalid, then propose std::vector<int> or another ownership-safe design before discussing optimisation. This compile, test and debug routine belongs in everyday Coding for Placements practice. It trains the precise explanation that a coding test or interview expects without assuming any employer's current format.
Short version and next step
Keep this five-item checklist:
Enable the strict warning set.
Initialise every value before reading it.
Make conversions intentional.
Run ASan and UBSan across the tests.
Rerun everything after each fix.
These tools find defects on compiled and executed paths, not every possible bug. For systematic language practice, continue with the C++ Programming course. Today, copy the two commands into your project, compile one small target, and resolve the first warning or sanitizer report before adding new code.




