CFoundations

C Control Flow & Deterministic Logic: if, switch, Loops & Goto (C23)

TT
TopicTrick Team
C Control Flow & Deterministic Logic: if, switch, Loops & Goto (C23)

C Control Flow & Deterministic Logic: if, switch, Loops & Goto (C23)


Table of Contents


Deterministic Logic: What Sets C Apart

In dynamically typed languages, even a simple if check may trigger property lookups, type coercions, or garbage collector activity. In C, the execution model is deterministic: the compiler translates your control flow into a fixed set of assembly instructions — CMP, JE, JNE, JMP — and the CPU executes them directly.

mermaid

This determinism is why C is mandated for real-time systems (car brake controllers, medical ventilators, aircraft flight management) where you need to mathematically prove the maximum execution time of a code path. There are no hidden allocations, no unexpected pauses — just predicable, measurable CPU cycles.


if-else: The Basics and Branch Prediction

The if-else statement is the most fundamental control flow construct. In C, any non-zero integer value is truthy; zero is false:

c

Understanding Branch Prediction

Modern CPUs are pipelined — they start fetching and decoding the next instruction before the current one finishes. When the CPU hits a conditional branch (if), it must predict which path to take. If it predicts correctly, the pipeline stays full and execution is fast. If it predicts wrong, the pipeline must be flushed and restarted — costing 10–20 CPU cycles.

This is called a branch misprediction penalty, and it is measurable. In tight loops processing millions of elements, poorly predicted branches can reduce throughput by 30-50%.


C23 Branch Prediction Hints: [[likely]] and [[unlikely]]

C23 introduces the [[likely]] and [[unlikely]] attributes, allowing you to give the compiler (and thus the CPU's branch predictor) explicit hints about which branch is statistically more common:

c

The compiler uses these hints to arrange the machine code so the likely path requires no jump (falling through sequentially), while the unlikely path requires a jump. This improves instruction cache efficiency and helps the CPU branch predictor make better guesses.

[!TIP] Use [[likely]] only when you have evidence (profiling data, domain knowledge) that the branch is taken >90% of the time. Incorrect hints actually make performance worse.


The switch Statement: Jump Tables Under the Hood

A switch statement over integer values can be dramatically faster than a long if-else if chain because the compiler can generate a jump table — a static array of function pointers (code addresses) that the CPU jumps to directly:

c

Jump Table Mechanics

For a switch with n sequential cases, the compiler generates code roughly equivalent to:

c

A long if-else if chain is O(n) — on average, you check half the conditions before finding the right one. For a state machine with 20 states, a switch is ~10× faster.

switch Fallthrough

Without break, execution falls through to the next case. This is occasionally useful:

c

[!WARNING] Accidental fallthrough is one of the most common bugs in C. Always use break. In C23, the [[fallthrough]] attribute documents intentional fallthrough: [[fallthrough]]; // intentional.


for Loops: The Mechanics of Iteration

The C for loop has three clauses: init; condition; increment. All three are optional:

c

Loop Counter Types: int vs size_t

A subtle but important point: use size_t for indices into arrays and int for counters with meaning. size_t is the type returned by sizeof and strlen — it is guaranteed to be large enough to index any array on the current platform. Mixing signed and unsigned loop counters produces compiler warnings and subtle comparison bugs.


while and do-while: Event-Driven Service Loops

The while loop checks the condition before the loop body. The do-while executes the body at least once before checking:

c

break, continue, and Loop Control

break exits the entire loop. continue skips the rest of the current iteration and moves to the next:

c

For nested loops, break only exits the innermost loop. To break out of multiple levels, you need either a sentinel flag, a function return, or — intentionally — goto.


The Rehabilitation of goto: Centralized Error Cleanup

goto has a terrible reputation from the 1970s "spaghetti code" era. In high-level languages, it is rightly avoided. In C systems programming, however, goto serves one specific, legitimate purpose: centralized error cleanup.

Without goto, error handling in C leads to deeply nested or duplicated cleanup code:

c
c

This pattern is used extensively in the Linux kernel and is the idiomatic way to handle resource cleanup in C. The key rule: only jump forward, never backward. Backward goto creates the "spaghetti" problem.


Look-Up Tables: Replacing Logic with Data

One of the most powerful performance optimizations in C is replacing complex logic with a pre-calculated array. When the output depends only on the input with no side effects, a Look-Up Table (LUT) replaces 10+ CPU cycles of computation with a single memory access:

c

LUTs are foundational in embedded systems (CRC checksums, sin/cos tables for motor control), audio processing (waveform generators), and network processing (protocol dispatch tables).


Loop Unrolling and Performance

When the compiler knows the iteration count, it can "unroll" the loop — replacing the loop with repeated copies of the body to reduce branch overhead:

c

GCC and Clang perform this automatically with -O2 and -O3 for fixed-size loops. You can hint at this with #pragma GCC unroll N or by using SIMD intrinsics, which we cover in the Advanced C++ module.


Frequently Asked Questions

Is goto really used in professional C code? Yes, extensively. The Linux kernel source (6+ million lines of C) uses goto thousands of times for exactly the error-cleanup pattern described above. git grep "goto" drivers/ in the kernel returns tens of thousands of hits. The criticism of goto applies to jumping backward or across initialization — not to the forward-jump cleanup pattern.

What is "undefined behavior" in the context of control flow? Certain C control flow constructs trigger undefined behavior (UB): falling off the end of a non-void function without returning a value, accessing an array out of bounds in a loop condition, or modifying the same variable twice between sequence points. The compiler assumes UB never occurs and may optimize in ways that produce surprising — and dangerous — results.

When should I use switch vs if-else? Use switch when you are comparing a single integer variable against a set of known constant values — the compiler can generate a jump table and it reads more cleanly. Use if-else when the conditions are complex (ranges, multiple variables, non-integer types) or when the number of branches is very small (1-2).

Why does C have do-while when while exists? do-while guarantees at least one execution of the loop body, which is semantically important for "parse and validate" patterns. It also maps directly to a specific CPU instruction pattern where the branch check is at the bottom of the loop — this is slightly more efficient when you know the loop body will always execute at least once.

What is the difference between break in a loop vs break in a switch? In a loop, break exits the loop. In a switch, break exits the switch block. If you have a switch nested inside a loop, break exits only the switch — not the loop. Use a flag variable or goto to break out of the loop from inside a switch.


Key Takeaway

Control flow in C is Transparent. You can predict exactly which assembly instructions the compiler will generate. By understanding jump tables, branch prediction, and when to use goto vs nested conditions, you write code that is not only logically correct but physically efficient on the CPU.

The C programmer who understands branch prediction speaks the same language as the hardware. That is why C remains the language of choice for performance-critical systems, from operating system kernels to embedded real-time controllers.

Read next: Functions & The Call Stack: Stack Frames and Performance →


Part of the C Mastery Course — 30 modules from foundations to expert systems programming.