Control Flow
Conditional and selection statements, loop forms, loop control, nested loops, control tracing, and termination conditions.
Structured programming replaces arbitrary jumps with a small set of composable forms: sequence, selection, and iteration. The goal is not merely to make execution go somewhere; it is to let a reader determine why every path exists and why every loop eventually stops.
Sequential Flow
Within a block, statements normally execute in order:
double subtotal = unit_price * quantity;
double tax = subtotal * tax_rate;
double total = subtotal + tax;
Each statement establishes state used by the next. Meaningful intermediate names reveal the calculation better than one dense expression.
Control statements alter which statement executes next, but they should preserve a visible overall structure.
Conditional Statements
Simple Decisions
if executes a statement only when its controlling expression compares unequal to zero:
if (temperature < 0.0) {
puts("freezing conditions");
}
Use braces consistently. They prevent later edits from accidentally leaving a statement outside the condition.
Two-Way Decisions
if-else selects exactly one branch:
if (balance >= withdrawal) {
balance -= withdrawal;
puts("approved");
} else {
puts("insufficient funds");
}
The condition should state the decisive fact. Avoid comparing a Boolean result with true:
if (is_ready) { /* clearer than is_ready == true */ }
Else-If Chains
An else-if chain represents ordered classification:
char grade;
if (mark >= 80) {
grade = 'A';
} else if (mark >= 70) {
grade = 'B';
} else if (mark >= 60) {
grade = 'C';
} else if (mark >= 50) {
grade = 'D';
} else {
grade = 'F';
}
Order matters. After mark >= 80 fails, the next branch may assume mark < 80. Putting mark >= 50 first would absorb all passing marks.
Validate the overall domain separately:
if (mark < 0 || mark > 100) {
fputs("mark must be between 0 and 100\n", stderr);
} else if (mark >= 80) {
/* ... */
}
Nested Conditions
Nesting is appropriate when a second decision is meaningful only inside the first:
if (account_exists) {
if (password_matches) {
grant_access();
} else {
record_failed_attempt();
}
} else {
report_unknown_account();
}
Deep nesting increases cognitive load. Guard clauses in functions can reject invalid cases early, and compound Boolean expressions can sometimes combine conditions. Do not flatten decisions when separate branches need distinct actions or explanations.
Dangling Else
An else belongs to the nearest unmatched if, regardless of indentation. Braces remove ambiguity:
if (outer) {
if (inner) {
act();
}
} else {
recover();
}
Selection Statements
switch selects a labelled branch from an integer or enumeration expression:
switch (choice) {
case 'a':
case 'A':
add_record();
break;
case 'l':
case 'L':
list_records();
break;
case 'q':
case 'Q':
puts("goodbye");
break;
default:
puts("unknown command");
break;
}
case labels require integer constant expressions and must be unique after conversion. switch does not directly match strings, ranges, or arbitrary Boolean conditions.
Fallthrough
Without a terminating transfer such as break or return, execution continues into the next labelled statement. Multiple labels can intentionally share a body, as the upper- and lowercase commands do above.
Accidental fallthrough is a classic defect:
case 1:
puts("one");
/* missing break: execution continues */
case 2:
puts("two");
When deliberate fallthrough contains code before the next label, document it clearly using the convention recognised by your project’s compiler and reviewers.
Switch or If
Choose switch for equality against several constant integral choices. Choose an else-if chain for ranges, compound predicates, strings, or conditions that are not all tests of one expression.
While Loops
A while loop tests before each iteration:
int value = 12345;
int digits = 0;
while (value > 0) {
value /= 10;
digits++;
}
If the condition is false initially, the body executes zero times. This makes while suitable when repetition depends on state not naturally expressed as a counter.
Sentinel Loops
A sentinel value or input status can terminate repetition:
int ch;
while ((ch = getchar()) != EOF) {
putchar(ch);
}
The assignment is deliberately parenthesised to make the intended comparison explicit. The loop obtains new input every iteration; forgetting the update creates an infinite loop.
Do-While Loops
A do-while loop tests after the body, so the body executes at least once:
int choice;
do {
puts("1. Continue");
puts("0. Quit");
choice = read_choice();
} while (choice != 0);
The required semicolon after while (condition); is part of the syntax. Use this form when one iteration genuinely precedes the continuation decision, not merely to avoid initializing a variable.
For Loops
A for loop groups initialization, continuation test, and update:
for (int i = 0; i < 10; i++) {
printf("%d\n", i);
}
Its execution order is:
initialization
↓
condition ──false──► exit
│ true
▼
body
↓
update
└──────────────► condition
The loop variable i is scoped to the loop when declared in the initializer. Prefer the half-open range 0 <= i && i < count for indexing: it starts at the first valid index and stops one past the last.
Any clause may be empty, but emptiness should be intentional:
for (;;) {
/* infinite loop with an explicit internal exit */
}
A for loop communicates counted or progression-based repetition. A while loop communicates repetition governed by a condition. They can express equivalent mechanics, but intent should guide the choice.
Change the start, limit, and step below, then advance one transition at a time to see exactly when initialization, testing, body execution, and update occur.
Nested Loops
Nested loops naturally model grids and combinations:
for (int row = 0; row < 3; row++) {
for (int column = 0; column < 4; column++) {
printf("(%d,%d) ", row, column);
}
putchar('\n');
}
The inner loop completes all four columns for each outer row, producing twelve body executions. Use distinct names that express each dimension.
Do not assume two nested loops always imply quadratic work. Bounds matter: an inner loop that advances monotonically across the entire outer process may execute only a linear number of times overall. Formal algorithm analysis belongs to the Algorithms course, but precise counting begins with a correct trace.
Loop Control
Break
break exits the nearest enclosing loop or switch:
for (int attempt = 1; attempt <= 3; attempt++) {
if (authenticate()) {
puts("accepted");
break;
}
}
A named state variable may be necessary after the loop if later code must distinguish successful termination from exhausted attempts.
Continue
continue skips the remainder of the current iteration and begins the next test/update cycle:
for (int i = 0; i < count; i++) {
if (values[i] < 0) {
continue;
}
total += values[i];
accepted++;
}
In a for loop, continue proceeds to the update expression, then the condition. In a while loop it proceeds directly to the condition. Ensure state needed for progress is not skipped.
Use break and continue to simplify a loop’s main path, not to create many hidden exits. If control becomes difficult to describe, extract a function or redesign the loop condition.
Control Tracing
Trace state at loop boundaries. For the Euclidean greatest-common-divisor loop:
int a = 48;
int b = 18;
while (b != 0) {
int remainder = a % b;
a = b;
b = remainder;
}
| Iteration | a before | b before | remainder | a after | b after |
|---|---|---|---|---|---|
| 1 | 48 | 18 | 12 | 18 | 12 |
| 2 | 18 | 12 | 6 | 12 | 6 |
| 3 | 12 | 6 | 0 | 6 | 0 |
The next condition is false, and a is 6.
A trace table should include every object that influences the next condition or externally visible result. It turns “I think it works” into a checkable execution argument.
Loop Invariants
A loop invariant is a property true before every condition check. It connects iterations into a correctness explanation.
For a loop that sums values[0] through values[count - 1]:
long total = 0;
for (size_t i = 0; i < count; i++) {
total += values[i];
}
a useful invariant is:
Before each test,
totalequals the sum of elements at indices0throughi - 1.
- Initialization: with
i == 0, that prefix is empty and its sum is zero. - Maintenance: adding
values[i]extends the covered prefix by one element. - Termination: when
i == count, the prefix is the entire array.
The invariant also exposes boundary errors: starting at 1, using <= count, or updating i incorrectly would break the argument.
Termination Conditions
Every deliberate finite loop needs a progress measure that moves toward an exit.
For for (int i = 0; i < count; i++), the measure count - i decreases each iteration while remaining non-negative. For Euclid’s loop, the non-negative second operand decreases under the required input conditions.
Questions to ask:
- Can the condition be false initially?
- Which statement changes the condition’s state?
- Can
continueskip that change? - Can arithmetic overflow prevent progress?
- Is end-of-file treated as termination rather than invalid input to retry forever?
Intentional infinite loops should make their exit mechanism visible through break, return, a signal, or an external event.
Digit Case
The following complete program reads one non-negative integer and reports its decimal digit count and digit sum. The task is small, but it demonstrates how a loop is derived from state, not guessed from syntax.
#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
int main(void) {
char line[128];
fputs("non-negative integer: ", stdout);
fflush(stdout);
if (fgets(line, sizeof line, stdin) == NULL) {
fputs("no input\n", stderr);
return EXIT_FAILURE;
}
char *start = line;
while (isspace((unsigned char)*start)) {
start++;
}
if (*start == '-') {
fputs("expected one non-negative integer\n", stderr);
return EXIT_FAILURE;
}
errno = 0;
char *end;
unsigned long original = strtoul(start, &end, 10);
if (end == start || errno == ERANGE) {
fputs("invalid or out-of-range integer\n", stderr);
return EXIT_FAILURE;
}
while (isspace((unsigned char)*end)) {
end++;
}
if (*end != '\0') {
fputs("expected one non-negative integer\n", stderr);
return EXIT_FAILURE;
}
unsigned long remaining = original;
unsigned long digit_sum = 0;
size_t digit_count = 0;
do {
unsigned long digit = remaining % 10;
digit_sum += digit;
digit_count++;
remaining /= 10;
} while (remaining != 0);
printf("%lu has %zu digit%s; digit sum = %lu\n",
original,
digit_count,
digit_count == 1 ? "" : "s",
digit_sum);
return EXIT_SUCCESS;
}
Why do-while? Zero has one written decimal digit, but a pre-test loop while (remaining != 0) would execute zero times for input zero. The post-test form processes the current least-significant digit once before asking whether any digits remain.
For input 5070, trace only the state that determines progress and the result:
| Iteration | remaining before | digit | sum after | count after | remaining after |
|---|---|---|---|---|---|
| 1 | 5070 | 0 | 0 | 1 | 507 |
| 2 | 507 | 7 | 7 | 2 | 50 |
| 3 | 50 | 0 | 7 | 3 | 5 |
| 4 | 5 | 5 | 12 | 4 | 0 |
At every condition check, digit_sum equals the sum of digits already removed from the original value, digit_count equals how many were removed, and remaining contains the unprocessed prefix. Dividing a positive integer by ten makes it smaller, so remaining reaches zero. The zero case terminates after the first iteration because it is already zero after division.
The parser skips leading whitespace and rejects a minus before calling strtoul. That conversion is permitted to accept a sign and convert the resulting value according to unsigned rules; syntax accepted by a library conversion is not automatically accepted by the application’s domain.
Partitioning Decisions
An else-if chain should partition the input domain into cases that are easy to state. Suppose a percentage mark is valid from 0 through 100:
if (mark < 0 || mark > 100) {
puts("invalid");
} else if (mark >= 80) {
puts("A");
} else if (mark >= 70) {
puts("B");
} else if (mark >= 60) {
puts("C");
} else if (mark >= 50) {
puts("D");
} else {
puts("F");
}
After the first condition is false, later branches may assume 0 <= mark <= 100. After mark >= 80 is false, the next branch effectively tests 70 <= mark < 80. The order turns single comparisons into disjoint intervals.
Test the boundaries where control changes, not only typical middle values:
-1, 0, 49, 50, 59, 60, 69, 70, 79, 80, 100, 101
If the chain tested mark >= 50 first, every passing mark would take that branch and the higher grades would be unreachable. Each condition may be locally true yet the overall ordering may still be wrong.
Loop Construction
Before writing for, while, or do-while, fill in five blanks:
- State: which objects describe completed work and remaining work?
- Initialization: what values represent “nothing processed yet”?
- Continuation: exactly when is another iteration required?
- Progress: which operation moves state toward completion?
- Result: what does the invariant imply when continuation becomes false?
For a bounded retry loop, also distinguish three exits: success, exhausted attempts, and an external stop such as end-of-file. A single Boolean may not retain enough information for the code after the loop. Use a named status or return from a small function when that makes the exits explicit.
Nested loops need the same reasoning at two levels. The inner loop completes one row, record, or local search for the current outer state. On every new outer iteration, inner state must be reinitialized. Accidentally declaring or initializing the inner index outside the outer loop is a common reason only the first row is processed.
Progress Under continue
This loop never changes value when it is negative:
while (value != 0) {
if (value < 0) {
continue;
}
value--;
}
The continue path violates the progress argument. Possible repairs depend on intent: reject negative state before the loop, update it before continuing, or redesign the condition. The right repair comes from the contract, not from mechanically moving value--.
A progress measure need not decrease by exactly one. It needs a well-founded direction that cannot continue forever under the preconditions: remaining input records decrease as they are consumed, an interval narrows, or a positive quotient shrinks. Arithmetic overflow and unsigned wrap can destroy an otherwise convincing measure, so type reasoning remains part of control-flow reasoning.
State Machine
Some control flow is clearer as named states and transitions than as loosely related Boolean flags. Imagine a small media player with stopped, playing, and paused states. Commands do not have the same meaning everywhere:
| Current state | Command | Next state | Effect |
|---|---|---|---|
| stopped | play | playing | start output |
| playing | pause | paused | suspend output |
| paused | play | playing | resume output |
| playing or paused | stop | stopped | reset position |
| any | quit | finished | leave loop |
A switch on current state can contain a second switch or focused function for commands. The design first asks whether a transition exists, then performs its effect and publishes the next state. Invalid commands leave state unchanged and report why.
Avoid encoding state redundantly:
bool is_playing;
bool is_paused;
bool is_stopped;
Eight Boolean combinations exist, although only three are meaningful. One state variable makes impossible combinations unrepresentable under the interface’s update discipline. Enumerations later provide names for such integral states.
The main event loop then has an invariant: before each input attempt, the state is one of the declared valid states. EOF, quit, input error, and invalid command are distinct transitions. A transition table is both design documentation and a source of tests.
Guarded Paths
Deep nesting makes readers remember many conditions simultaneously:
if (record != NULL) {
if (record->count > 0) {
if (record->count <= limit) {
/* main work */
}
}
}
Inside a function, guard clauses can state rejected cases first:
if (record == NULL) {
return false;
}
if (record->count == 0 || record->count > limit) {
return false;
}
/* main work under established assumptions */
Multiple returns are not inherently unstructured. They are useful when no acquired resource needs cleanup and each return completes the function’s contract clearly. When resources must be released, converge paths on one cleanup region or use smaller functions so guard clauses precede acquisition.
An else is unnecessary after a branch that always returns, breaks, or continues. Removing it reduces indentation without changing reachability. Do not remove else when the alternatives form one policy whose symmetry aids understanding.
Branch Coverage Map
Derive control tests from each decision boundary. For:
if (age < 0) {
status = INVALID;
} else if (age < 13) {
status = CHILD;
} else if (age < 20) {
status = TEEN;
} else {
status = ADULT;
}
use -1, 0, 12, 13, 19, 20, plus extreme representable values if input conversion permits them. Each adjacent pair straddles a transition. Branch coverage that merely executes every body can still miss an incorrect operator at a boundary; expected-value assertions matter.
For compound conditions, test short-circuit paths:
pointer != NULL && pointer->ready
requires at least null, non-null/not-ready, and non-null/ready cases. The null case should demonstrate that the right operand is not evaluated, ideally through a design where evaluation would be invalid rather than through an extra side effect added only for testing.
Nested Coordinate Trace
For two nested loops:
for (int row = 0; row < 2; row++) {
for (int column = 0; column < 3; column++) {
printf("(%d,%d) ", row, column);
}
}
the execution order is:
(0,0) (0,1) (0,2) (1,0) (1,1) (1,2)
For each outer iteration, the inner declaration creates a fresh column initialized to zero. Total body executions are 2 * 3. If bounds come from input, multiplication may overflow when estimating work even though the nested loops themselves would simply run for an impractically long time. Validate resource and time limits at the same boundary as numeric ranges.
break exits only the innermost loop. Exiting both loops can use a status checked by the outer condition, a return from a focused search function, or a controlled jump to cleanup when resources are involved. Choose a shape that states the real exit condition rather than duplicating it across bodies.
Search Exit
A search loop has two ordinary termination reasons: a match was found or the range was exhausted. Preserve that distinction explicitly:
size_t index = 0;
while (index < count && values[index] != target) {
index++;
}
if (index < count) {
printf("found at %zu\n", index);
} else {
puts("not found");
}
The condition orders the bound check first. When index == count, short-circuiting prevents values[index] from being read. On exit, one comparison index < count identifies the reason; a separate found flag would duplicate state.
The invariant is: no element in [0, index) equals the target. Progress increments index, shrinking the unexamined suffix. At match exit, values[index] is the target. At exhaustion, the invariant covers the whole range.
Loop Choice Table
Choose syntax from when the condition and progress are naturally visible:
| Situation | Natural form | Reason |
|---|---|---|
| counted half-open range | for | initialization, bound, and update stay together |
| retry until external condition | while | continuation is known before each attempt |
| menu that must display once | do-while | first body execution is required |
| event service with explicit exits | for (;;) | no false top-level condition is invented |
Any form can simulate the others. The goal is to expose the proof. A for loop with unrelated updates in all three clauses or a while whose progress is hidden far below its condition works mechanically but increases audit cost.
Control Complexity
Every active nested branch adds a fact readers must remember. Extracting a named predicate can reduce that load:
bool can_submit = is_registered && has_payment && !is_suspended;
if (can_submit) {
submit();
}
The name must match the expression; otherwise it hides rather than clarifies. For complex policies, separate predicates can be tested at their boundaries and combined at one decision point.
Avoid clever fallthrough, assignments inside conditions, and multiple state-changing expressions merely because C permits them. Experienced technical readers value locally provable control more than syntactic density.
Control Failures
- Assignment in a condition by accident: distinguish
=from==and heed warnings. - Misleading indentation without braces: braces, not indentation, control nesting.
- Wrong else-if order: test narrower or higher-priority cases before broader ones.
- Accidental switch fallthrough: terminate or document every case intentionally.
- Off-by-one bounds: prefer half-open ranges and state valid indices.
- Stale loop state: ensure every path obtains or computes the next condition value.
- A
continuethat skips progress: update before continuing or choose aforupdate clause. - Unsigned countdown underflow:
for (size_t i = count - 1; i >= 0; i--)cannot terminate as expected.
Flow Reasoning
- Sequence, selection, and iteration form the core structured-control vocabulary.
ifhandles predicates and ranges;switchhandles equality against integral constants.whiletests before,do-whiletests after, andforexposes a progression compactly.breakexits the nearest loop or switch;continueadvances to the next iteration.- Trace tables reveal exact state transitions and boundary behaviour.
- Loop invariants explain partial results; progress measures explain termination.
- Clear control flow minimises nesting, hidden exits, and state changes that readers must remember.
Flow Problems
Read the Branches
- When is
switchmore suitable than an else-if chain? - State the minimum possible body-execution count for each loop form.
- Where does
continuetransfer control inforandwhileloops?
Follow the State
- Trace the Euclidean loop for
a = 270,b = 192. - Trace a nested 3-by-2 loop and list the
(row, column)pairs in output order. - Predict the output of a switch with a missing
breakbetween two nonempty cases.
Repair the Flow
- Correct a grading chain that tests
mark >= 50beforemark >= 80. - Repair
for (size_t i = count - 1; i >= 0; i--)for reverse iteration, including the empty case. - Find why a
whileinput loop repeats forever after conversion failure.
Shape the Loop
- Print a multiplication table with labelled rows and columns using nested loops.
- Read integers until end-of-file, counting positive, negative, and zero values separately.
- Implement an input menu with
switch; accept upper- and lowercase commands without duplicating action bodies.
Prove Termination
- State and justify a loop invariant for finding the maximum value in a nonempty array.
- Give a termination measure for a loop that repeatedly divides a positive integer by two until it becomes zero.
- Refactor a deeply nested validation sequence into a function with clear guard clauses. Compare the number of active conditions a reader must track.