5.1.30 · D5C Programming

Question bank — Undefined behavior — comprehensive list, why to avoid

1,861 words8 min readBack to topic

Before we start, one anchor you must hold in your head for every answer:

The one picture to hold for the whole page: your program lives inside a "valley" of well-defined states. Every UB trap is a cliff edge — step over it and there is no ground, no rule about where you land.

Figure — Undefined behavior — comprehensive list, why to avoid

True or false — justify

True or false: unsigned integer overflow is undefined behavior.
False. Unsigned overflow is defined — it wraps modulo . Only signed overflow is UB. See Integer Types and Overflow.
True or false: signed overflow is fine as long as the CPU uses two's complement.
False. The hardware wrapping is irrelevant; the standard says signed overflow is UB, so the optimizer may assume it never happens regardless of what the CPU would do.
True or false: INT_MIN / -1 is safe because dividing by just flips the sign.
False. It is UB: is one larger than , so the mathematically correct result cannot be represented — a signed overflow.
True or false: dividing by zero is UB in C.
True for integers — x / 0 and x % 0 are UB. (Floating-point division by zero is a separate, IEEE-governed story, not covered here.)
True or false: i = i++; is guaranteed to leave i as either 5 or 6.
False. Modifying i twice with no sequence point ordering them is UB, so the result is anything at all, not a choice between two values. See Sequence Points and Operator Precedence.
True or false: reading an uninitialized int just gives you an unpredictable-but-real number.
False. It is UB — the compiler may assume the variable is never read, and two reads of the same variable can even yield different values or a trap representation.
True or false: a program that passes all tests under -O0 contains no UB.
False. UB can hide until a new compiler, -O2, or a new input activates the "never happens" assumption. Passing tests proves nothing about UB.
True or false: it is legal to form a pointer one past the end of an array, but not to dereference it.
True. The standard specifically allows the one-past pointer to exist for loop bounds; reading or writing through it is UB.
True or false: modifying a string literal like char *s = "hi"; s[0]='H'; is fine because s is a valid pointer.
False. String literals may live in read-only memory and the standard makes modifying them UB — the pointer being valid does not make the write valid.
True or false: printf("%d", 3.5) prints 3 after truncation.
False. A format/argument-type mismatch in varargs is UB; printf reads the bits as an int from the wrong place on the stack, so the output is meaningless, not a rounded 3.

Spot the error

if (x + 100 < x) return -1; — what's wrong as an overflow check for signed x?
Since signed overflow is UB, the compiler assumes it never happens, so x + 100 < x is always false and the whole check gets deleted. Check with x > INT_MAX - 100 in the defined domain instead.
int *bad(void){ int local=42; return &local; }
What is undefined about using the returned pointer?
local is an automatic variable whose lifetime ends when bad() returns; the returned pointer is dangling. Reading *p afterwards is UB even if 42 "happens" to still be there. See Pointers and Memory in C.
free(p); free(p);
Why is this UB?
The second free is a double free — after the first free, p no longer points to a live allocation, so freeing it again corrupts the allocator's bookkeeping. See malloc and free — Dynamic Memory.
free(p); printf("%d", *p);
What rule is broken?
Use-after-free: the storage's lifetime ended at free, so dereferencing p is UB. NULL the pointer after freeing to make the bug crash loudly instead.
int a[3]={1,2,3}; printf("%d", a[3]);
Why is a[3] a bug even though it printed a number?
Valid indices are 0..2; a[3] reads out of bounds — UB. Printing "a number" is just one possible symptom; it may also crash or corrupt an adjacent variable silently.
int v; unsigned n = ...; int r = v << n;
Which values of n make this UB?
n >= width_in_bits of int (e.g. >= 32), and also left-shifting a negative v. Since n is unsigned it can never be negative, so the danger is purely the too-large or negative-v cases; only shifts inside 0..width-1 on a suitable value are defined. (For a signed shift count, n < 0 would add another UB case.)
An int foo(int x) function ends with } and no return on some path; the caller uses the result. Where is the UB?
Falling off the end of a non-void function and then using the returned value is UB — the returned "value" doesn't exist. (Not returning is only fine if the value is never used.)

Why questions

Why does a UB on line 50 sometimes break line 10?
Because the optimizer reasons backwards and forwards from "this path never runs," it can delete or reorder earlier code (like a check or a printf) that leads into the UB path — the whole program's meaning becomes undefined, not just one line.
Why did the C committee leave so many things undefined instead of specifying them?
For speed (no forced runtime checks), portability (no freezing C to one CPU's quirks), and optimization licence (the compiler may assume UB never occurs). It buys "portable assembly" performance.
Why is unsigned overflow defined but signed overflow not?
Unsigned arithmetic has one obvious, hardware-independent answer — wrap mod . Signed representations historically differed (sign-magnitude, ones'/two's complement), so the standard left signed overflow undefined rather than pick one. See Integer Types and Overflow.
Why can't I trust "it ran correctly" as proof my code is UB-free?
A correct-looking run is one accidental outcome among the infinitely many UB permits. Change the compiler, flags, or input and the compiler's assumptions bite. Use UBSan/ASan instead of trusting a run.
Why does enabling -O2 sometimes "introduce" a bug that wasn't there at -O0?
The bug (the UB) was always present; -O2 gives the optimizer more freedom to act on the "never happens" assumption — deleting checks or reordering — so the latent UB finally manifests. See Compiler Optimizations and -O flags.
Why is accessing an object through an incompatible pointer type UB even if the bytes line up?
The strict aliasing rule lets the compiler assume differently-typed pointers never touch the same object, enabling caching/reordering. Violating it makes those optimizations produce wrong results. See Strict Aliasing Rule.
Why is relying on the evaluation order of f(g(), h()) risky, and how is f(i++, i++) worse?
Argument evaluation order is unspecified (some allowed order happens, but you don't know which). f(i++, i++) goes further: it modifies i twice with no sequence point between them, which is undefined — anything may happen.

Edge cases

Is an infinite loop while(1); with no side effects safe in C11?
Not reliably — C11 lets the compiler assume side-effect-free loops terminate, so it may optimize the loop (and code around it) in surprising ways. Add a volatile side effect if you truly need to spin.
Is forming (not dereferencing) a pointer two past the end of an array UB?
Yes. Only one past the end is a valid pointer to form; going further is UB even without dereferencing. Loop bounds must stop at exactly one-past.
Is p = NULL; p + 1; — pointer arithmetic on a null pointer — UB?
Yes. The standard only defines pointer arithmetic within (or one past) a real array object; a null pointer points to no object, so even computing p + 1 (never mind dereferencing) is UB. Check for NULL before doing arithmetic. See Pointers and Memory in C.
Is dereferencing a pointer that isn't correctly aligned for its type UB?
Yes. If you cast a char* at an odd address to int* and read through it, you violate the type's alignment requirement — UB, even on x86 where it "usually works." On stricter CPUs (some ARM) it faults outright. Use memcpy to move bytes into a properly aligned object instead.
Is x % 0 UB the same way x / 0 is?
Yes — integer modulo by zero is UB just like integer division by zero; they compute together on most hardware and neither has a defined result.
If unsigned int n = 0; n--; — is that UB?
No. Unsigned arithmetic wraps mod , so n becomes UINT_MAX — this is fully defined behavior, which is exactly why unsigned is the right tool when you want wraparound.
Does casting a valid void* back to its original type and dereferencing that cause UB?
No — dereferencing a void* directly is the error; round-tripping through the correct object type is fine. The UB is using the wrong type, not using void* for storage.

Recall Fast self-quiz

Signed overflow defined? ::: No — UB. Unsigned overflow defined? ::: Yes — wraps mod . Can UB affect earlier lines? ::: Yes — the compiler reasons backward from "never happens." Two runtime tools to catch UB? ::: UBSan and ASan (-fsanitize=undefined,address). Is one-past-the-end pointer legal to form? ::: Yes to form, no to dereference. Is NULL + 1 UB? ::: Yes — arithmetic on a pointer that points to no object.