---
title: 'Control Flow'
source: 'https://academia.sh/en/courses/c-fundamentals/control-flow'
course: 'C Fundamentals'
language: en
updated: '2026-09-26T03:02:41+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Measures that which code path a branch follows depends on data produced at runtime'
    - "Distinguishes switch fall-through, loop forms and goto's limited use with examples"
    - 'Shows the difference between which branches exist and which one runs, through the preprocessor and runtime stages'
---

# Control Flow

Which code path a branch follows being determined by data produced at runtime; switch fall-through, loop forms, goto's limited place, and the distinction between which branches exist and which one is chosen.

The Operators lesson measured what determines an expression's result, and the answer was
always the same: the types and structure written in the source were already known before
a run ever started. Control flow reverses this order. Which branch a program follows, how
many times a loop runs — these can no longer be told in advance by reading the source;
they are known only once the program runs and produces the data.

The Programming Fundamentals course (M01/K02) already established `if`, `while`, and
similar control structures with Python; there is a superficial resemblance between that
syntax and this one, but this lesson measures not syntax but **at which stage a decision
is made**: whether a branch is chosen or not depends on data, while which branches exist
in the source can depend on a separate stage entirely.

This lesson is a turning point for M11's measurement axis. The Data Types, Variables, and
Operators lessons all pointed in the same direction: the types and structure written in
the source made the decision before a run ever began. In control flow this direction
reverses — whether a branch is chosen is now known not by reading the source but by
running the program and producing the data. But this reversal is not absolute: as will be
seen below, which branches even exist in the source can still be decided at a stage
before the compiler, at the preprocessor. Control flow is therefore a subject that sits
not in a single class, but at the intersection of two.

## Data-Dependent Branching

The excerpt below is part of the core introduced in the Compilation Stages lesson: an
array's sum is computed, and a branch is chosen by looking at that sum.

```c
// stage.c — data-dependent branching (excerpt)
#include <stdio.h>
#include <stddef.h>

int main(void) {
    int data[] = {4, 8, 15, 16};
    int sum = 0;
    for (size_t i = 0; i < sizeof data / sizeof data[0]; i++) {
        sum += data[i];
    }
    const char *verdict = (sum > 40) ? "large" : "small";
    printf("%s %s\n", "data-dependent branch", verdict);
    return 0;
}
```

```
data-dependent branch large
```

**BS7.** Whether the `verdict` variable will be `"large"` or `"small"` is not written
anywhere in the source — it cannot be known until the four numbers in the `data` array
are summed. Because `sum` comes out `43`, the `"large"` branch is chosen, but the result
could change if a single element were added to the array or one element changed — with
the source code itself never changing. This is a decision diametrically opposed to the
operator choice measured in the previous lesson: there, types determined the result and
it was known before a run started; here, data determines the result and it can only be
known once the loop has completed.

This difference also shows up in the `?:` conditional operator itself. Which branch of
`(sum > 40) ? "large" : "small"` produces a value depends on the run, but that the
expression itself produces a value is already known at compile time — the compiler
derives a common result type from the two branches (here both are the same, so `const
char *`), and it knows one of the two will be chosen, only not which one. This shows a
general property of control flow: the source fixes at compile time which branches exist
and which type of result each branch will produce; only which one runs, decided by
looking at the data, is left to runtime. The same distinction holds for `if` bodies and
`switch` cases: every branch in the source is compiled and takes its place in the binary,
but which one actually executes on a given run can only be told after that run has
finished.

## Switch Fall-Through

In a `switch` statement, consecutive `case` labels **fall through** into the same body
unless a `break` separates them — flow passes from one label to the next without being
interrupted. **Fall-through here corresponds to what was closed in M06/K05** — this usage
is entirely separate from that lesson's `drop` sense; C has no such concept as `drop` at
all.

```c
// fallthrough.c — switch fall-through binds consecutive labels to the same body
#include <stdio.h>

static void print_level(int score) {
    switch (score / 10) {
        case 10:
        case 9:
            printf("%s\n", "level: A");
            break;
        case 8:
        case 7:
            /* deliberate fall-through: 8 and 7 fall into a shared body */
            printf("%s\n", "level: B");
            break;
        default:
            printf("%s\n", "level: C");
            break;
    }
}

int main(void) {
    print_level(95);
    print_level(88);
    print_level(72);
    print_level(40);
    return 0;
}
```

```
level: A
level: B
level: B
level: C
```

When `score / 10` gives `10`, flow enters the bodiless `case 10` label, finds no body,
and falls through into `case 9`, running the body there; when it gives `9`, it jumps
directly into `case 9`. The two labels thus share a single body. The same arrangement
applies to `case 8` and `case 7`. Flow continuing when no `break` is written is C's
default behavior; every `case` that wants to exit a body must state so **explicitly**
with `break`. The four calls above confirm this: `95` matches the bodied `case 9` and
never falls through at all; `88` matches the bodiless `case 8` and falls through into
`case 7`'s body.

Fall-through itself is always present in the source regardless of which `case` matches —
the compiler produces a path flowing from every label without a `break` to the next one,
and this path sits in the binary. But whether this path is followed is determined at
runtime, purely by the value at the head of the `switch` statement: when `95` is entered,
flow does not start from `case 10` at all, it jumps directly to `case 9`, because
`switch` leaps to a matching label the instant it finds one and runs only from that point
onward, in sequence. Fall-through therefore does not mean "every label is tried in
sequence" — `switch` first finds the matching label, and fall-through only passes through
the labels **after** that point, bodiless, if no `break` separates them.

## Loop Forms: When the Condition Is Tested

C carries three basic loop forms, and the difference between them is whether the
condition is tested **before** or **after** the loop body — and this leads to a
data-dependent difference in behavior.

```c
// loops.c — while and do-while interpret the same condition with a different run count
#include <stdio.h>

int main(void) {
    int counter = 5;
    int while_run_count = 0;
    while (counter < 5) {
        while_run_count++;
        counter++;
    }
    printf("%s %d\n", "while run count", while_run_count);

    int do_while_run_count = 0;
    do {
        do_while_run_count++;
    } while (counter < 5);
    printf("%s %d\n", "do-while run count", do_while_run_count);
    return 0;
}
```

```
while run count 0
do-while run count 1
```

Because `counter` starts at `5` and `while (counter < 5)` tests its condition **before**
the body, the condition is false from the very start and the body never runs at all:
`while_run_count` stays at zero. `do { ... } while (counter < 5)`, by contrast, runs the
body first and tests the condition **after**; even though the condition is again false,
the body has already run once, and `do_while_run_count` comes out `1`. Both loops carry
the same condition and run on the same data, but because the testing order differs,
their results diverge — this difference depends not on where the condition is written in
the source, but on which form the loop is declared in, and a `for` loop tests its
condition before the body just like `while` does.

All three forms have their body written only once in the source, but how many times it
runs depends on the data at runtime — if `counter`'s starting value were different, the
same three loops would run a different number of times. The difference between `while`
and `for` is purely a matter of notation: `for (start; condition; step)` gathers a
`while` loop's three parts onto a single line, and both follow the same testing order
(condition first). `do-while` is the one form that differs: when a loop's body must run
at least once regardless of the condition — showing a menu, prompting for input — this
form expresses that directly, without needing a separate flag variable or writing the
body twice.

## Goto: A Limited and Accepted Use

`goto` jumps directly to a label from anywhere in the source, and its general use is
discouraged because it makes flow harder to follow. But in a narrow scenario like exiting
nested loops in a single step, `goto` offers a widely accepted solution, because `break`
can only break the nearest loop.

```c
// search.c — goto is a limited and accepted use for exiting a nested loop in one step
#include <stdio.h>

int main(void) {
    int grid[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
    int target = 6;
    int found_i = -1, found_j = -1;

    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            if (grid[i][j] == target) {
                found_i = i;
                found_j = j;
                goto found;
            }
        }
    }
found:
    printf("%s %d\n", "found row", found_i);
    printf("%s %d\n", "found column", found_j);
    return 0;
}
```

```
found row 1
found column 2
```

The value of `target` is found at `grid[1][2]`, and `goto found;` leaves both loops in a
single line. Code trying to achieve the same result with `break` would have to exit the
inner loop, then check a flag variable to break the outer loop separately as well —
`goto` does the same job here without needing an extra flag variable. That the `found`
label sits outside the loops, near the end of the function, is also part of the accepted
usage: `goto` only jumps forward, to a point where the function's natural flow ends, and
does not build a complex web of jumps in the middle of the source.

Whether the `found` label is actually jumped to is also a data-dependent decision: if the
value of `target` were never found inside `grid`, both loops would run to completion and
`goto` would never run at all, flow reaching the label only in sequence, at the loops'
natural end. In the source, the `goto found;` line always sits in the same place, but
whether that line runs — and if it does, at which loop step — is determined at runtime
by looking at `grid`'s contents and `target`'s value. `goto` therefore does not depart
from control flow's general rule: the jump itself is fixed in the source, whether the
jump is triggered depends on the data.

## The Bounding Measurement

Which code path a branch follows depended on data, but which branches exist in the
source is not always determined by runtime — sometimes the preprocessor, before the
compiler ever sees the source, has already decided which `case`s will even exist.

```c
// boundary.c — which branches exist is settled at the preprocessor, which one is chosen at runtime
#include <stdio.h>

#define EXTRA_CASE

static void evaluate_code(int code) {
    switch (code) {
        case 1:
            printf("%s\n", "case 1 ran");
            break;
#ifdef EXTRA_CASE
        case 2:
            printf("%s\n", "case 2 ran (preprocessor confirmed its existence)");
            break;
#endif
        default:
            printf("%s\n", "unrecognized case");
            break;
    }
}

int main(void) {
    for (int code = 1; code <= 3; code++) {
        evaluate_code(code);
    }
    return 0;
}
```

```
case 1 ran
case 2 ran (preprocessor confirmed its existence)
unrecognized case
```

Because the `EXTRA_CASE` macro is defined, the `case 2` label is included into the text
by the preprocessor before the compiler ever sees the source. Had `EXTRA_CASE` not been
defined, that `case` block would have been erased from the source entirely — the compiler
would never see it, and flow would fall straight to `default` when `code == 2` arrived.
In this measurement, `case 2` exists because `EXTRA_CASE` is defined, and as `code`
advances from `1` to `3` in the loop, which case is chosen is determined at runtime at
every step by looking at the data (the loop counter). The two decisions are entirely
independent of each other: whether a branch exists is decided at the **preprocessor**,
whether that branch is chosen is decided at **runtime** — and if a branch never exists at
all, no data can choose it.

The order of these two stages cannot be reversed. The preprocessor runs first and either
erases `#ifdef` blocks from the source entirely or leaves them entirely; the compiler
already knows which `case`s exist in the text that reaches it and never makes a decision
like "let this case exist for this data." Runtime, in turn, works under the opposite
constraint: it cannot change which branches already exist in the binary it has, it can
only choose among the ones that exist. This distinction produces a practical consequence
when tracking down a bug: "this branch never ran" and "this branch does not exist in the
source at all" are two separate possible causes, and where each is checked also differs —
one is looked for in the runtime output, the other in the source's preprocessor
directives.

## Summary

- Which code path a branch follows is determined by looking at data produced at
  **runtime**; the source code itself does not make this decision in advance — class
  **runtime**.
- `case` labels inside a `switch` with no `break` **fall through** into the next body;
  this fall-through is C's default behavior unless `break` is written explicitly in the
  source.
- `while`'s condition is tested before the body, `do-while`'s after; even with the same
  condition and the same data, this difference in order can change the run count.
- `goto`, in a narrow and accepted scenario like exiting nested loops in one step, does
  the same job without requiring an extra flag variable.
- Which branches exist in the source can be decided at the **preprocessor**, and which of
  the existing branches is chosen can be decided at **runtime** — both decisions can
  coexist within the same `switch` and are independent of each other.
- The `?:` conditional operator fixes at compile time that it produces a value and that
  value's type; the type is derived from the two branches, only which branch actually
  produces the value is determined at runtime.

## Next Step

The branches measured in this lesson all stayed within the body of a function already
known — which branch was chosen depended on data, but which function was called was
always fixed in the source. The Functions lesson tests this last assumption: which
definition a call is bound to is resolved only at the linking step, after two units have
been compiled separately. A function like `evaluate_code` above was always defined in the
same file; in the next lesson, a function will be called in one unit while its definition
sits in another — and what determines the link between the two, and at which stage, will
be settled there. In this lesson, the result of a test like `sum > 40` depended only on
the `data` array; in the next lesson, a call's result will also depend on which
definition the called function reaches — a name written in the source cannot correspond
to two different bodies in two different units, but only the linker knows which body it
is bound to.
