Exercises — Undefined behavior — comprehensive list, why to avoid
Level 1 — Recognition
Goal: given a snippet, decide "UB / defined / unspecified / implementation-defined" and name the category.
Recall Solution L1.1
- (a) Defined. Unsigned arithmetic wraps modulo (here ), so
ubecomes . This is a guarantee, not luck. See Integer Types and Overflow. - (b) UB. Signed integer overflow.
INT_MAX + 1has no defined result — the optimizer may assume it never happens. - (c) UB. Division by zero. There is no "returns 0" rule; it is undefined (often traps on x86).
- (d) Implementation-defined.
sizeof(int)is a documented choice of the compiler (commonly 4). - (e) Unspecified. The order of evaluating
g()andh()is unspecified — the compiler may run either first, and need not tell you. (It becomes UB only if the two sub-expressions clash on the same object without a sequence point — see Sequence Points and Operator Precedence.)
Recall Solution L1.2
True — UB. "hi" is a string literal; writing to it is undefined (literals may live in read-only memory). The pointer read s[0] would be fine; the write is the crime. Fix: char s[] = "hi"; copies into a writable array.
Level 2 — Application
Goal: predict what the optimizer is licensed to do, and rewrite to a defined form.
Recall Solution L2.1
(a) Signed overflow is UB, so the compiler assumes x + 1000 never overflows. Under that assumption x + 1000 < x is always false → the whole if body is dead code and gets removed. Your guard vanishes at -O2.
(b) Reason in the defined domain — compare before adding:
if (x > INT_MAX - 1000) return INT_MAX; // no addition overflows here
return x + 1000;Now no overflow ever occurs, the comparison is meaningful, and the compiler cannot delete it. See Compiler Optimizations and -O flags.
Recall Solution L2.2
- (a) Defined. Forming a one-past-the-end pointer is explicitly allowed. Look at figure s01: the dashed slot at index 5 is a legal address.
- (b) UB. This dereferences index 5 — one past the last valid element (0..4). Reading it is undefined.
- (c) UB. Even forming
&a[6](two past the end) is undefined — you may only go one past. - (d) Defined.
a + 4is the last valid element; dereferencing index 4 is fine.

Level 3 — Analysis
Goal: explain WHY a program that "printed the right answer" is still broken, tracing the compiler's reasoning.
Recall Solution L3.1
local is an automatic variable — its lifetime ends the instant bad() returns. The returned pointer is now dangling: it names a stack slot the program is free to reuse. Reading *p accesses an object outside its lifetime → UB.
It printed 42 only because nothing had yet overwritten that slot — pure timing luck. A different optimization level, a stack-protector, or one extra function call between bad() and the printf changes the answer or crashes. Figure s02 shows the slot being recycled.
"Works once" is the single most dangerous symptom of UB because it hides the bug until production. Fix: return by value, or malloc and let the caller free — see malloc and free — Dynamic Memory.

Recall Solution L3.2
At line (A) the code dereferences p. Dereferencing NULL is UB, so the compiler infers: "the programmer promised p is not NULL — otherwise (A) is UB, which I may assume never happens." Carrying that promise forward, p == NULL at (B) must be false, so the compiler deletes the if.
This is UB reasoning propagating downstream: the symptom (missing null check at B) is not on the buggy line. Fix: check before the first dereference:
if (p == NULL) return -1;
int x = *p;Level 4 — Synthesis
Goal: combine multiple UB rules; audit a whole function and fix every defect.
Recall Solution L4.1
Three defects:
- Out-of-bounds write.
i <= 4runsi = 0..4;buf[4]is past the last valid index (0..3). Writing it is UB. → usei < 4. - Shift + signed overflow.
r << k: ifkmakes the value exceedINT_MAX, left-shifting a value into the sign bit / out of range is UB, and left-shifting a negativeris UB. Alson % 32is fine only ifn != 0(see #3). Do the shift onunsigned. - Division by zero +
INT_MIN / -1.sum / nis UB whenn == 0(div-by-zero) and also whensum == INT_MIN && n == -1(overflow). Both must be guarded.
Corrected:
int process(int n) {
if (n == 0) return 0; // guard div-by-zero and n%0
int buf[4];
for (int i = 0; i < 4; i++) // fix bounds
buf[i] = i * i;
int r = 0;
for (int i = 0; i < 4; i++)
r += buf[i]; // r = 0+1+4+9 = 14
unsigned shift = (unsigned)(n % 32);
unsigned sum = (unsigned)r << shift; // defined wraparound
if (r == INT_MIN && n == -1) return INT_MAX; // guard INT_MIN/-1
return (int)sum / n;
}With the loop fixed, buf = {0,1,4,9} and r = 14. For n = 2: n % 32 = 2, sum = 14 << 2 = 56, return 56 / 2 = 28. Run it under Sanitizers — ASan and UBSan to confirm zero diagnostics.
Recall Solution L4.2
Unspecified vs UB: With f(g(), h()), the order of the two calls is unspecified, but each call is a self-contained, sequenced piece of work touching different state — no conflict. With f(i++, i++), the same object i is modified twice with no sequence point ordering the two modifications relative to each other → UB (Category 3). The result is not "some order of 5 and 6" — it is anything.
Fix — introduce ordering by hand:
int a = i++; // sequenced
int b = i++; // sequenced after a
f(a, b); // now defined: passes original i, then i+1Level 5 — Mastery
Goal: reason like a language lawyer and a compiler simultaneously; design UB-free interfaces.
Recall Solution L5.1
We must detect overflow before performing the addition, because performing it first would already be UB (and the optimizer could delete our test — recall L2.1).
bool safe_add(int a, int b, int *out) {
if (b > 0 && a > INT_MAX - b) return false; // a+b would exceed INT_MAX
if (b < 0 && a < INT_MIN - b) return false; // a+b would go below INT_MIN
*out = a + b; // now provably in range
return true;
}Why these forms are defined: INT_MAX - b with b > 0 cannot underflow (subtracting a positive from the max stays in range); INT_MIN - b with b < 0 cannot overflow (subtracting a negative from the min stays in range). Each guard is computed entirely inside the safe domain, so the compiler cannot dismiss it as UB-dependent.
Checks: safe_add(INT_MAX, 1, &o) → false. safe_add(2000000000, 200000000, &o) → false. safe_add(100, 200, &o) → true, o == 300. safe_add(INT_MIN, -1, &o) → false.
Recall Solution L5.2
This violates the strict aliasing rule — see Strict Aliasing Rule. The rule says an object of type float may not be read through an int lvalue (int and float are incompatible types). The compiler assumes pointers of unrelated types never point at the same storage, so it may reorder or elide the accesses. Equal sizeof is irrelevant — it is a type rule, not a size rule.
Fix — use memcpy (or a union, per C11):
int bits;
memcpy(&bits, &myfloat, sizeof bits); // byte copy, no aliasing violationmemcpy accesses the bytes as unsigned char, which is allowed to alias anything, so the reinterpretation is well-defined.
Active Recall
Recall Quick self-check
Is UINT_MAX + 1 UB? ::: No — unsigned overflow is defined, it wraps to 0.
Why can a NULL-check after a dereference be deleted? ::: The dereference is UB on NULL, so the compiler assumes the pointer is non-null and removes the redundant test.
May you form &a[n] for an int a[n]? ::: Yes, forming the one-past pointer is defined; dereferencing it (a[n]) is UB.
Safe way to detect signed overflow in a+b? ::: Compare before adding: b>0 && a>INT_MAX-b, or b<0 && a<INT_MIN-b.
Correct way to reinterpret a float's bits as int? ::: memcpy (or a union) — a pointer cast violates strict aliasing.