JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

The Common Traps: Overflow, Bounds, Uninitialized

The last guide told you what undefined behavior is and why the optimizer may delete your code. This one is the field guide to the three traps you will actually fall into — signed overflow, reading out of bounds, and using an uninitialized value — shown small, concrete, and with the surprising ways they fail.

From the rule to the traps

In guide 2 you learned the hard truth about undefined behavior: it is not "whatever the compiler feels like" and it is not the same as "platform-dependent." The C standard simply imposes no requirements at all on a program that executes a UB operation, and the optimizer is allowed to assume UB never happens. That assumption is the source of the spooky behavior you met — code that vanishes at `-O2`, branches that get deleted, the time-travel effect where a consequence of UB shows up before the line that triggers it. This guide turns that abstract rule into a field guide: the three traps that produce most real-world UB, what each one looks like in tiny code, and why it bites.

Keep one mental model from here on. UB is the compiler and you signing a contract: you promise never to do the forbidden thing, and in exchange the compiler promises fast code that need not check for it. When you break the promise, you don't get a tidy error — you get whatever fell out of optimizations that trusted you. The three traps below, signed integer overflow, out-of-bounds access, and uninitialized reads, are the three places beginners break the contract most often, so they deserve a careful, honest look one at a time.

Trap one: signed integer overflow

Here is the trap that surprises nearly everyone, because it hinges on one distinction from the binary rung: signed and unsigned overflow are not the same in C. Unsigned arithmetic wraps — it is defined to compute modulo 2^N, so an `unsigned int` that hits its top and adds one quietly becomes 0, by the rules. But signed overflow is undefined behavior. When an `int` at its maximum, 0x7fffffff, gains one more, the standard does not say it becomes the most negative number — it says nothing happens that you can rely on. The hardware might wrap to a negative; the optimizer might do something stranger, because it is allowed to assume the overflow never occurs.

Why does that matter beyond a wrong number? Because that assumption gives the optimizer real power to delete your safety checks. Consider a check written after the addition: `if (x + 1 < x) { /* overflow! */ }`. You meant to catch wrap-around. But the compiler reasons: "`x + 1 < x` can only be true if `x + 1` overflowed; signed overflow is UB; I may assume UB never happens; therefore this condition is always false." It deletes the whole branch. Your overflow check is gone at `-O2`, because you used overflow to detect overflow — exactly the kind of reasoning from guide 2. The honest lesson: never test for signed overflow by letting it happen. Check before you compute.

  // WRONG: tries to detect overflow by letting it happen.
  // signed overflow is UB, so the optimizer may delete the branch.
  int add_bad(int x) {
      if (x + 1 < x) return -1;   // may be compiled away at -O2
      return x + 1;
  }

  // RIGHT: check BEFORE the operation, no UB is ever executed.
  #include <limits.h>
  int add_ok(int x) {
      if (x > INT_MAX - 1) return -1;   // would overflow? bail first
      return x + 1;
  }
Detect overflow by testing the operands against the limit (INT_MAX) before you add — never by inspecting the overflowed result afterward.

Trap two: reading or writing out of bounds

The second trap is the most famous, and it follows straight from how arrays and pointers work. In C an array is just a run of bytes with no length attached, and `a[i]` is defined to mean `*(a + i)` — pure pointer arithmetic. The language gives you no automatic bounds check: if `a` has 4 elements, the indices 0, 1, 2, 3 are valid, and `a[4]` or `a[-1]` is out-of-bounds access — undefined behavior. The standard permits forming a pointer to one past the last element (you may compute `a + 4` to mark the end), but you must never dereference it. Read or write past the edge and you are off the contract entirely.

Where do these come from in practice? Overwhelmingly from the humble off-by-one error: a loop written `for (i = 0; i <= n; i++)` instead of `i < n`, so the last iteration touches `a[n]`, one slot too far. Or a string buffer sized for the text but not for the terminating `'\0'` of a null-terminated string. The treachery is that out-of-bounds rarely crashes immediately. `a[4]` often lands on the next live variable or on the allocator's hidden bookkeeping, so the program limps on with one value silently corrupted, and the segfault — if it comes at all — comes later, somewhere innocent. "It didn't crash" is not "it was fine."

And out-of-bounds is not only a correctness bug, it is the classic security hole. When the out-of-bounds write spills into adjacent memory chosen by an attacker's input — overrunning a stack array into the return address, or a heap block into the next chunk — you have a buffer overflow, the single most exploited class of memory bug in C's history. That whole story is the subject of the very next guide, where you will see exactly why functions like `gets()` are banned outright. For now, hold the core fact: in C, you are the bounds check. Nothing does it for you.

Trap three: the uninitialized read

The third trap is the quietest, because the broken value looks like an ordinary value. When you write `int x;` with automatic storage — a local variable on the stack — C does not zero it for you. It simply hands you whatever bytes happened to be sitting in that stack frame from a previous call. Reading `x` before you have assigned it is using an uninitialized variable, and that read is undefined behavior. The same is true of memory from malloc(): it returns a block whose contents are indeterminate. (Only static and global variables, and calloc(), start zeroed.)

Beginners expect this to mean "I get a random number," which would be merely a bug. The reality is worse and weirder. Because an uninitialized read is UB, the optimizer may assume it never produces a meaningful value, and in practice an uninitialized variable can read as different values at different points in the same function, or cause a branch to go both ways in the compiler's reasoning. A favorite real symptom: the program works at `-O0`, where the stack slot happened to hold 0, and corrupts at `-O2`, where the optimizer kept the variable in a register that held leftover garbage. "It works on my machine" is the catchphrase of this trap, because the leftover bytes differ from run to run and build to build.

A habit for catching all three

These three traps share a cruel personality: they are silent, they depend on the optimization level, and they often corrupt far from where they were born. That makes "just read the code carefully" a weak defense on its own. The good news is that the same tooling rung you just climbed gives you machines that catch exactly these. The UndefinedBehaviorSanitizer, compiled in with `-fsanitize=undefined`, traps signed overflow and bad shifts the instant they execute and prints the file and line. The AddressSanitizer (`-fsanitize=address`) catches out-of-bounds reads and writes on the stack and heap. And the MemorySanitizer hunts down uninitialized reads.

  1. Build with warnings always on: `gcc -O2 -Wall -Wextra` catches many uninitialized and overflow mistakes at compile time, for free.
  2. For test runs, add the sanitizers: `gcc -O1 -g -fsanitize=address,undefined main.c` turns the silent traps into a loud, located crash.
  3. Check overflow BEFORE you compute (test against INT_MAX), never by inspecting an already-overflowed result.
  4. Treat every array access as your responsibility: track the length yourself and never let an index reach the size.
  5. Initialize every variable at declaration — `int x = 0;`, `char *p = NULL;` — so an uninitialized read can never happen.

Hold on to the honest framing from this whole rung. These are not exotic, expert-only mistakes; they are the everyday slips that the most experienced C programmers still make, which is exactly why the tooling exists. The point is not to feel that C is treacherous and give up, but to internalize where the contract's edges are and to build the reflexes — bounds you track, values you initialize, overflow you check up front — that keep you on the right side of them. With those reflexes, the next guide can take you into the sharpest specific case: how an out-of-bounds write becomes a buffer overflow, and why one old function earned a permanent ban.