Most programmers think undefined behavior means “the program might crash.” It means something worse: the program might do anything — crash, silently corrupt data, format your hard drive in principle — and the compiler is allowed to assume it never happens. That assumption is where things get dangerous.
What Undefined Behavior Actually Means
The C standard defines three levels of behavior:
- Well-defined: the standard specifies exactly what happens
- Implementation-defined: the behavior is unspecified, but the compiler must document what it does
- Undefined: anything can happen — the standard places no requirements whatsoever on the compiler
When the compiler sees code that triggers UB, it is allowed to assume that code path is never taken. It then optimizes accordingly. This is where silent bugs come from.
Signed Integer Overflow
#include <stdio.h>
int is_big(int x) {
return x + 1 > x; /* always true? */
}
int main() {
printf("%dn", is_big(2147483647)); /* INT_MAX */
return 0;
}
You might expect this to print 0 — because INT_MAX + 1 wraps to a large negative number, making the comparison false. With GCC at -O2, it prints 1.
Why? Signed integer overflow is undefined behavior. The compiler knows that if x is an int, x + 1 > x is trivially true by the laws of arithmetic (assuming no overflow). Since UB cannot happen in a well-formed program, the compiler concludes the comparison is always true and optimizes the function to return 1.
This is not a bug in the compiler. The programmer invoked UB; the compiler acted on it.
Null Pointer Dereference
#include <stdlib.h>
#include <stdio.h>
void process(int *ptr) {
*ptr = 42; /* UB if ptr is NULL */
if (ptr == NULL) { /* dead code: compiler knows ptr is non-null here */
abort();
}
printf("%dn", *ptr);
}
int main() {
process(NULL);
return 0;
}
The null check after the dereference is removed by the optimizer. Since dereferencing a null pointer is UB, the compiler assumes ptr is never null (otherwise you would have UB). The null check becomes dead code and is deleted. The crash happens before the “safety” check that was meant to prevent it.
Reading Uninitialised Variables
#include <stdio.h>
int main() {
int x;
if (x > 0) { /* UB: reading uninitialised variable */
printf("positiven");
}
return 0;
}
The value of x is whatever bytes happened to be at that stack address. The compiler can assume x is any value — including one that makes the if branch always taken or always skipped, whichever produces smaller code.
Out-of-Bounds Array Access
#include <stdio.h>
int arr[5] = {0, 1, 2, 3, 4};
int main() {
for (int i = 0; i <= 5; i++) { /* reads arr[5] — one past the end */
printf("%dn", arr[i]);
}
return 0;
}
Accessing arr[5] is UB. The compiler is not required to keep the loop counter comparison accurate — if it determines that every iteration accesses valid memory (because the program is well-formed), it may remove or transform the bounds check entirely. You get a segmentation fault — or worse, you silently read adjacent memory.
Strict Aliasing Violations
#include <stdio.h>
int main() {
float f = 3.14f;
int *p = (int*)&f; /* strict aliasing violation */
*p = 0;
printf("%fn", f); /* result is undefined */
return 0;
}
The C standard says you may not access an object through a pointer of an incompatible type (with a few exceptions for char*). Compilers use this rule to prove that a write through an int* cannot affect a float object — so they do not reload f from memory after the *p = 0 write. The result is not 0.0 — it is whatever was cached in a register.
The correct way to type-pun is with memcpy, which is defined behavior:
#include <string.h>
float f = 3.14f;
int i;
memcpy(&i, &f, sizeof(i)); /* defined: copies bytes, no aliasing issue */
How to Catch UB Before It Bites You
/* Compile with sanitizers — they catch UB at runtime */
gcc -g -fsanitize=undefined -fsanitize=address -o program program.c
AddressSanitizer catches out-of-bounds reads and writes. UndefinedBehaviorSanitizer catches signed overflow, null dereference through pointers, misaligned access, and more. Run your test suite with these enabled. They add a 2-5× slowdown but find bugs that normal execution silently ignores.
/* What UBSAN output looks like */
program.c:4:14: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
The Rule of Thumb
Never rely on overflow, uninitialized values, or out-of-bounds access producing predictable results. If you need wraparound arithmetic, use unsigned types — unsigned overflow is defined behavior (it wraps modulo 2^n). If you need to read uninitialized memory intentionally, that is almost always a design error.
The compiler is not your enemy when it exploits UB — you told it the code was correct. Use sanitizers, compile with -Wall -Wextra, and test on multiple compilers. Our c compiler uses GCC with sanitizers enabled, which is a good starting point for checking your code.
TL;DR
- Undefined behavior is not “probably a crash” — the compiler assumes it never happens and optimizes accordingly
- Signed integer overflow, null dereference, uninitialized reads, out-of-bounds access, and strict aliasing violations are the most common triggers
- The optimizer legally removes “dead code” that only runs on UB paths — including your safety checks
- Use
-fsanitize=undefined,addressduring development to catch UB at runtime - Use
unsignedtypes when you need defined wraparound arithmetic - Never dereference a pointer before null-checking it