Menu

C++ Undefined Behavior: What It Is and How to Avoid It

Undefined behavior (UB) is code the C++ standard places no rules on - it may crash, corrupt data, or appear to work. Learn the common causes, why "it ran fine" proves nothing, and the tools that catch UB.

This page includes runnable editors - edit, run, and see output instantly.

What "Undefined Behavior" Actually Means

The previous page showed how try/catch handles errors your program defines and throws on purpose. Undefined behavior is the opposite: it's the set of operations the C++ standard refuses to give any meaning at all. There is no exception to catch, no error code, no guarantee of a crash. The compiler is free to assume UB never happens and to do whatever it likes when it does.

That freedom is what makes UB so dangerous. The same buggy line might print the "right" answer on your laptop, return garbage on a server, and get deleted entirely by the optimizer at -O2. UB is not "behavior we don't document" - it's "behavior the language promises nothing about." Your job is to never write it in the first place.

int arr[3] = {1, 2, 3};
int x = arr[5];   // undefined behavior: reading past the end of the array

There's no compiler error here, and on many runs it will quietly hand you a stray integer. That apparent success is the trap.

Reading or Writing Out of Bounds

The most common form of UB is touching memory you don't own. Built-in arrays and std::vector::operator[] do no bounds checking - an index past the end (or a negative one) is instant UB, whether you read or write.

The bug to watch for is <= where you meant <: when i == v.size() you index one past the last element, which is UB. Prefer a range-based for (covered earlier) when you don't need the index, since it can't run off the end. When you do index by hand and want a safety net, v.at(i) throws std::out_of_range instead of silently corrupting memory:

Use at() while you're hunting a bug; switch back to [] in hot loops once you've proven the indices are valid.

Dangling Pointers and Use-After-Free

A pointer or reference that outlives the object it points at is dangling. Using it is UB - the memory may have been reused, freed, or never have existed. This is the trap that smart pointers (from the previous chapter) help you avoid, but raw pointers still let you fall into it.

The sharpest version is returning the address of a local variable. The local dies when the function returns, so the caller is left holding a pointer to nothing:

int* makeNumber() {
    int n = 42;
    return &n;   // returns address of a local - it's gone after return
}
// Dereferencing the result is undefined behavior.

The same thing happens after delete or when a vector reallocates and invalidates iterators or pointers into it:

int* p = new int(5);
delete p;
cout << *p;   // use-after-free: undefined behavior

vector<int> v = {1, 2, 3};
int* first = &v[0];
v.push_back(4);   // may reallocate - 'first' is now dangling
cout << *first;   // undefined behavior

The defenses are the ones you already know: keep objects alive as long as any pointer needs them, prefer references and smart pointers over raw owning pointers, and re-fetch pointers/iterators after any operation that can resize a container.

Uninitialized Variables and Signed Overflow

Reading a variable before you've given it a value is UB for built-in types - there is no default 0. The variable holds whatever bits were already in that memory, and the optimizer may assume you never read it uninitialized.

If sum had been declared as a bare int sum;, every sum += i would read an indeterminate value first - UB, and a notoriously hard bug because it often looks like it works. Make initialization a habit: int x = 0; or int x{};.

Another silent offender is signed integer overflow. Pushing a signed int past its maximum is UB (unsigned types wrap around predictably; signed types do not):

int big = 2147483647;   // INT_MAX on a 32-bit int
int oops = big + 1;     // signed overflow: undefined behavior

Don't rely on it "wrapping to a negative number" - the compiler is allowed to assume overflow can't happen and may optimize based on that. If you need defined wraparound, use an unsigned type or check the bounds before you add.

Catching UB With Sanitizers and Warnings

You cannot test your way to confidence about UB, because a passing run guarantees nothing. What does work is making UB loud at runtime with the compiler's sanitizers (available in GCC and Clang).

// AddressSanitizer: out-of-bounds, use-after-free, leaks
g++ -fsanitize=address -g -O1 main.cpp -o app && ./app

// UndefinedBehaviorSanitizer: signed overflow, null deref, bad casts
g++ -fsanitize=undefined -g main.cpp -o app && ./app

Run your existing tests under these flags and the out-of-bounds read, the use-after-free, or the signed overflow that "worked fine" turns into a precise report naming the file and line. Combine them with -Wall -Wextra so the compiler also flags suspicious code (like a likely-uninitialized read) before you even run it.

==1234==ERROR: AddressSanitizer: heap-use-after-free on address 0x...
READ of size 4 at 0x... thread T0
    #0 main.cpp:7 in main

Treat any sanitizer report as a must-fix bug, not a warning to ignore - it's telling you the standard makes no promises about that line.

Wrap-Up

Undefined behavior is the part of C++ where the safety rails come off: out-of-bounds access, dangling pointers, use-after-free, uninitialized reads, and signed overflow all produce code with no defined meaning, and "it ran fine" is never proof that it's correct. The way to stay safe is to write defensively - initialize every variable, respect container bounds, let smart pointers own your heap memory - and then verify with -fsanitize=address, -fsanitize=undefined, and -Wall -Wextra so silent UB becomes a loud, fixable report.

That closes out the Errors & Debugging chapter. Between exceptions, try/catch, and a healthy fear of UB, you now have the tools to write C++ that fails loudly and on purpose rather than silently and by accident.

Frequently Asked Questions

What is undefined behavior in C++?

Undefined behavior (UB) is any operation the C++ standard explicitly leaves with no defined result - for example reading past the end of an array or dereferencing a dangling pointer. The compiler is allowed to do anything: crash, return garbage, optimize the code away, or appear to work today and break after a recompile. It is a bug in your program, not a feature of the language.

Why does my C++ program work even though it has undefined behavior?

"It ran fine" proves nothing about UB. The standard gives no guarantee either way, so a UB bug can produce the result you expected on your machine with your compiler today, then crash on a different optimization level, platform, or compiler version. Never treat a passing run as evidence that UB is harmless - use a sanitizer to actually catch it.

How do you catch undefined behavior in C++?

Compile with sanitizers: -fsanitize=address (AddressSanitizer) finds out-of-bounds reads/writes and use-after-free, and -fsanitize=undefined (UndefinedBehaviorSanitizer) flags signed overflow, null dereferences, and bad casts. Turn on warnings (-Wall -Wextra) and run your tests under these flags - they turn silent UB into a clear runtime report.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED