Undefined behavior (UB) is C’s most dangerous feature. The compiler can do literally anything when UB occurs—including appearing to work until it doesn’t.
Before looking at the dangers, understand why C was designed this way:The goal: Performance and portability above all else.Why UB exists:
Performance: Checking for errors adds overhead.
Example: Checking array bounds on every access makes code 10-30% slower.
C’s philosophy: “Trust the programmer” - if you say arr[100], C assumes you know what you’re doing.
Portability: Different hardware behaves differently.
Example: What happens when you right-shift a negative number?
x86: Arithmetic shift (preserves sign)
Some ARM/PowerPC: Logical shift (fills with zeros)
C’s solution: Make it “Implementation Defined” or “Undefined” so compilers can use the fastest native instruction.
Optimization: The compiler assumes UB never happens.
Example: If you write x + 1, the compiler assumes no overflow.
This allows it to optimize loops and algebraic simplifications that wouldn’t be valid if it had to handle overflow wrapping.
The tradeoff: You get maximum speed and can run on any hardware, but you lose safety. C is a sharp tool - it cuts through problems efficiently, but can also cut you.
The C standard defines three categories of problematic code:
Category
Meaning
Example
Undefined Behavior
Anything can happen. Compiler can assume it never occurs.
Signed overflow, null dereference
Unspecified Behavior
Valid behaviors, but implementation chooses
Order of function argument evaluation
Implementation-Defined
Must be documented by compiler
Size of int, right-shift of negative numbers
Undefined behavior is not just “unpredictable.” Modern compilers actively exploit UB for optimization. Code that “works” might break with a different compiler, optimization level, or even compiler version.
#include <stdio.h>#include <limits.h>// UNDEFINED BEHAVIORvoid overflow_demo(void) { int x = INT_MAX; x = x + 1; // UB! Signed overflow printf("%d\n", x); // May print anything, or not execute at all}// Real-world danger: loop optimizationint sum_to_n(int n) { int sum = 0; for (int i = 1; i <= n; i++) { // If n = INT_MAX, i overflows sum += i; // Compiler may assume loop terminates } return sum;}// This check can be REMOVED by the compiler!int bad_overflow_check(int x) { if (x + 1 < x) { // Compiler assumes this is always false (no UB) printf("Overflow!\n"); // Dead code elimination! } return x + 1;}// SAFE alternatives#include <stdint.h>// Use unsigned (wraps predictably)uint32_t safe_add_unsigned(uint32_t a, uint32_t b) { return a + b; // Wraps on overflow (defined behavior)}// Check before operationint safe_add_signed(int a, int b) { if ((b > 0 && a > INT_MAX - b) || (b < 0 && a < INT_MIN - b)) { // Handle overflow return 0; } return a + b;}// Use compiler builtins (GCC/Clang)int safe_add_builtin(int a, int b, int *result) { return !__builtin_add_overflow(a, b, result);}
#include <stdio.h>#include <stdlib.h>// UNDEFINED BEHAVIORvoid null_deref(void) { int *p = NULL; *p = 42; // UB! Dereferencing null pointer}// Dangerous pattern: check after dereferencevoid dangerous_check(int *p) { int x = *p; // Dereference first if (p == NULL) { // Compiler may remove this check! return; // (If we dereferenced, p "can't" be null) } printf("%d\n", x);}// SAFE: check before dereferencevoid safe_check(int *p) { if (p == NULL) { return; } int x = *p; // Safe now printf("%d\n", x);}
#include <stdio.h>// UNDEFINED BEHAVIORvoid sequence_violations(void) { int i = 0; i = i++; // UB! Modifying i twice between sequence points i = ++i + i++; // UB! Multiple modifications int arr[10]; arr[i] = i++; // UB! Which value of i? printf("%d %d\n", i++, i++); // UB! Order unspecified, but also UB}// Function arguments: order is UNSPECIFIED (not undefined)int f(int a, int b) { return a - b; }void unspecified_order(void) { int i = 0; int x = f(i++, i++); // UB! (modifying i twice) // But this is just unspecified: int a = 1, b = 2; int y = f(a, b); // Order of evaluation unspecified, but OK}// SAFE: separate statementsvoid safe_increment(void) { int i = 0; int old = i; i++; // Use old and i separately}
#include <stdio.h>#include <stdint.h>// UNDEFINED BEHAVIORvoid bad_shifts(void) { int x = 1; x << 32; // UB if int is 32 bits (shift >= width) x << -1; // UB! Negative shift amount int y = -1; y << 1; // UB! Left shift of negative number (until C23) y >> 1; // Implementation-defined (arithmetic or logical)}// SAFE shiftsvoid safe_shifts(void) { uint32_t x = 1; // Check shift amount int shift = get_shift(); if (shift >= 0 && shift < 32) { x <<= shift; // Safe } // Use unsigned for predictable behavior uint32_t mask = 1U << 31; // Safe, defined}
#include <stdio.h>// UNDEFINED BEHAVIORvoid pointer_violations(void) { int arr[10]; int *p = arr; p = p + 11; // UB! Past one-past-end p = p - 1; // UB! Before array start int *q = arr + 10; // OK: one-past-end is valid int x = *q; // UB! Can't dereference one-past-end int a, b; ptrdiff_t diff = &a - &b; // UB! Different objects int *null = NULL; null + 1; // UB! Arithmetic on null pointer}// Comparing pointers from different arraysvoid compare_violation(void) { int arr1[10], arr2[10]; if (arr1 < arr2) { // UB! Comparing unrelated pointers // ... } // Equality comparison is OK if (arr1 == arr2) { // Always false, but defined // ... }}
#include <stdio.h>#include <limits.h>// UNDEFINED BEHAVIORvoid division_ub(void) { int x = 10 / 0; // UB! Division by zero int y = 10 % 0; // UB! Modulo by zero // The most obscure UB: int z = INT_MIN / -1; // UB! Result overflows (INT_MIN = -2^31, INT_MAX = 2^31-1)}// SAFEint safe_divide(int a, int b) { if (b == 0) return 0; // Handle division by zero if (a == INT_MIN && b == -1) return INT_MAX; // Handle overflow return a / b;}
#include <stdio.h>#include <stdlib.h>// Compiler optimizations based on UB assumptions:// 1. Dead code eliminationvoid example1(int *p) { int x = *p; // If we reach here, p is non-null if (p == NULL) { // Compiler: "This is impossible" abort(); // Eliminated! }}// 2. Loop optimizationint example2(unsigned int n) { int sum = 0; for (int i = 0; i < n; i++) { // i can't overflow (UB) sum += i; // Compiler can use closed form } return sum; // Might become n*(n-1)/2 directly}// 3. Infinite loop eliminationint example3(void) { while (1) { // Nothing with side effects } return 0; // Compiler might assume this is reachable}// 4. Time travelint fermat(void) { int a = 1, b = 1, c = 1; while (1) { if (a*a*a + b*b*b == c*c*c) { return 1; // Found Fermat counterexample! } // ... increment a, b, c ... }}// Compiler might return 1 immediately!// (If loop has UB, compiler assumes it terminates)