C Control Flow: Branching, Loops, and Jumps

Write correct if/else and switch branches, choose the right loop construct (for, while, do-while) for a given problem, use break, continue, and return appropriately, and understand what goto is for and why it is rarely justified outside narrow cleanup patterns.

Control flow is what turns a flat list of statements into a real program: decisions, repetition, and early exits. C's control-flow vocabulary is small and, once again, entirely inherited by Objective-C — every if, every for loop, every switch over an integer works exactly the same way inside an Objective-C method body as it does in a plain C file. This lesson covers every branching and looping construct C offers, when to reach for each one, and the sharp edges (fallthrough switches, infinite loops, goto) that trip up newcomers.

if, else if, and else

The if statement executes a block only when its controlling expression is nonzero (true): if (condition) { ... }. An optional else block runs when the condition was false, and else if chains let you test a sequence of mutually exclusive conditions in order, stopping at the first one that matches: if (a) { ... } else if (b) { ... } else { ... }. Only one branch of an if/else-if/else chain ever executes per pass through it, and conditions are tested top to bottom — once one matches, the rest are skipped entirely, regardless of whether they would also have been true.

C allows omitting the braces { } around a single-statement body — if (x > 0) printf("positive\n"); is legal — but doing so is a well-known source of bugs when someone later adds a second statement intending it to be part of the conditional block and forgets to add braces, so it silently executes unconditionally instead. Virtually every professional C and Objective-C style guide requires braces on every if/else body, even single-line ones, specifically to prevent this class of bug (this is sometimes called avoiding 'dangling else' confusion, though that term more precisely refers to which if an else attaches to when braces are omitted in nested conditionals).

The controlling expression of an if can be any expression that produces a value, not just a comparison — if (count) is a common, idiomatic shorthand for if (count != 0), and if (pointer) is shorthand for if (pointer != NULL). Both are entirely legal and common in real C and Objective-C codebases, though some teams prefer the explicit comparison form for readability, especially for pointers where 'if (pointer)' could momentarily be misread as testing the pointed-to value rather than the pointer itself.

• if (condition) { ... } — runs the block only when condition is nonzero. • else / else if chains test conditions top-to-bottom; only the first match's block runs. • Always use braces {} even for single-statement bodies — prevents a classic class of bugs. • if (count) and if (pointer) are common shorthand for != 0 and != NULL respectively.

switch and Fallthrough

switch compares one integer (or char, or enum) expression against a series of constant case labels, jumping directly to the matching label: switch (value) { case 1: ...; break; case 2: ...; break; default: ...; }. The default label is optional and, if present, catches any value that didn't match an earlier case; it can appear anywhere in the switch body, though placing it last is the overwhelming convention.

The single most important thing to understand about switch in C is that execution falls through from one case to the next unless explicitly stopped with break — after a matched case's statements run, control continues into the following case's statements too, regardless of whether that case's own value matches. This is different from many newer languages where each case implicitly breaks. Omitting a break where one was intended is one of the most common and infamous C bugs; deliberately omitting it (intentional fallthrough) is a legitimate technique for handling multiple cases identically, but should always be marked with a comment like // fall through so a reader knows it's intentional rather than a bug.

switch only works with expressions that reduce to an integer type at compile time for their case labels (no strings, no floats, no ranges in standard C) — this is a real limitation compared to if/else if chains, which can test arbitrary boolean expressions including string comparisons via strcmp(). Choose switch when branching on a small fixed set of integer or enum values (it's often clearer and can be compiled more efficiently, e.g. as a jump table), and choose if/else if when the conditions involve ranges, strings, or compound boolean logic.

• switch (expr) { case A: ...; break; case B: ...; break; default: ...; } — expr must be an integer/enum type. • Execution falls through to the next case unless break stops it — this is the most common switch bug. • Intentional fallthrough should always be commented, e.g. // fall through. • switch cannot test strings, floats, or ranges — use if/else if for those. • default is optional, catches unmatched values, conventionally placed last.

for, while, and do-while Loops

The for loop packs initialization, a continuation condition, and an update step into one header: for (init; condition; update) { body }. It runs init once, then repeatedly checks condition before each iteration (stopping when it's false), runs the loop body, then runs update, and repeats. for is the natural choice whenever the number of iterations (or the counter's behavior) is known or bounded up front, such as iterating a fixed range or walking a C array by index: for (int i = 0; i < count; i++) { ... }.

while (condition) { body } checks condition before every iteration, including the very first — if the condition is false immediately, the body never runs even once. It's the natural choice when the number of iterations isn't known ahead of time and depends on something evaluated fresh each pass, such as reading input until a sentinel value appears, or walking a linked structure until a NULL terminator is reached: while (node != NULL) { ...; node = node->next; }.

do { body } while (condition); is like while, but checks the condition after the body instead of before, guaranteeing the body runs at least once regardless of the condition's initial value. This is the right tool specifically when 'run once, then decide whether to repeat' matches the problem, such as a menu loop that must display at least once before checking whether the user chose to exit, or a retry loop that must attempt an operation before it has anything to check.

• for (init; condition; update) { ... } — best when the iteration count/bound is known up front. • while (condition) { ... } — checks before each pass; body may run zero times. • do { ... } while (condition); — checks after each pass; body runs at least once. • Choose based on 'do I know the count?' (for), 'might it run zero times?' (while), or 'must it run at least once?' (do-while).

break, continue, and return

break immediately exits the innermost enclosing loop or switch statement, skipping any remaining iterations or cases entirely — control resumes at the statement right after the loop or switch. It's commonly used to stop a search loop the moment a match is found, avoiding wasted iterations: for (...) { if (found) break; }. break only affects the single innermost construct it's inside; breaking out of a nested loop from the inner loop does not also exit the outer loop (C has no labeled break like some other languages — that's achieved, when truly needed, with a flag variable or, rarely, goto).

continue skips the rest of the current iteration's body and jumps straight to the loop's next iteration check — in a for loop, that means the update step still runs before the condition is re-checked; in a while loop, it jumps directly to re-checking the condition. It's used to skip processing for elements that don't need it, without the extra nesting an if wrapping the rest of the body would require: for (...) { if (skip) continue; doWork(); }.

return immediately exits the current function altogether — not just the current loop or block — optionally carrying a value back to the caller (return value;) or, for a void function, with no value (return; or simply falling off the end of the function body). return used inside a loop is a common and perfectly idiomatic way to exit both the loop and the function in a single step once a definitive answer is found, avoiding an extra flag variable just to break out and then check it afterward.

• break — exits the innermost loop or switch immediately; does not affect outer loops. • continue — skips to the next iteration (for: runs update first; while: rechecks condition). • return — exits the entire function immediately, optionally with a value. • return inside a loop is a clean way to exit both the loop and function at once.

goto: What It Is and When It Is Legitimate

goto label; unconditionally jumps execution to a label: elsewhere in the same function, bypassing all normal structured control flow. It has a well-earned bad reputation — unrestricted goto use produces 'spaghetti code' that is extremely hard to trace, reason about, or refactor safely, which is why goto is absent from most modern languages entirely and is treated as a code smell in nearly every C style guide when used for general branching or looping (anything goto can do for ordinary control flow, if/else/for/while/break/continue can do more clearly).

There is, however, one narrow and genuinely legitimate use that shows up constantly in real, well-regarded C code (including large parts of the Linux kernel and other systems software): a single designated exit/cleanup label at the end of a function, used to guarantee that all previously acquired resources get released exactly once, no matter which of several error points the function bails out from. Without goto, a C function that allocates several resources in sequence and must release all of them on any failure ends up either duplicating the entire cleanup block at every failure point, or nesting increasingly deep if/else blocks — both are worse for correctness than a single cleanup label.

The pattern looks like this: allocate resource A; if that fails, return immediately (nothing to clean up yet). Allocate resource B; if that fails, goto cleanup_a; to free only A before returning. Allocate resource C; if that fails, goto cleanup_b; to free B then fall through to free A. On success, do the real work, then fall through the same cleanup labels on the way out. Every resource is freed exactly once, from exactly one place in the code, regardless of where the function actually exited — this is meaningfully clearer and less error-prone than the alternatives, which is exactly why this specific use is considered acceptable (and even good practice) by C programmers who otherwise avoid goto entirely. Objective-C code rarely needs this pattern directly (ARC and Cocoa's ownership model handle most cleanup automatically), but you will see it in the C and C-library code that sits underneath Objective-C frameworks, and understanding it is part of reading real systems-level C.

• goto label; jumps unconditionally to a label: elsewhere in the same function. • Avoid goto for general branching/looping — if/else/for/while/break/continue are always clearer for that. • Legitimate use: a single cleanup/exit label at function end, releasing resources acquired earlier on any failure path. • This pattern guarantees each resource is freed exactly once, from one place, regardless of which failure path was taken. • Common in low-level C and systems code (e.g. Linux kernel style); rare in day-to-day Objective-C thanks to ARC.

Nested Loops and Infinite Loops

Loops can nest inside one another freely — a for inside a for, a while inside a for, and so on — and each loop's break/continue only ever affects its own innermost enclosing loop, not any loop surrounding it. This is exactly why processing a 2D grid typically uses two nested for loops (one for rows, one for columns), and why breaking out of the inner loop when a condition is met does not, by itself, stop the outer loop from continuing to its next row.

An infinite loop is written deliberately with for (;;) { ... } or while (1) { ... }, both of which have no built-in stopping condition — the loop only ends via an internal break, a return, or the process being terminated externally. This isn't a mistake or edge case; it's the standard, idiomatic way to write an event loop, a server's request-handling loop, or a game's main loop, where the natural exit condition is checked and acted on (with break or return) from somewhere inside the loop body rather than in the loop header itself.

An accidental infinite loop, by contrast, is a genuine and common bug: a while loop whose condition variable is never updated inside the body, or a for loop whose update step accidentally never makes the condition false (for example, looping with an unsigned counter down to zero using >=, where an unsigned value can never go below zero and the comparison is always true). Always double check that a loop's condition variable is actually modified somewhere reachable inside the loop body, and that the modification moves it toward the stopping condition.

• Nested loops: inner break/continue only affects the innermost loop, never an enclosing outer loop. • for (;;) or while (1) — the idiomatic, intentional way to write an infinite event/server/game loop. • An intentional infinite loop exits via an internal break or return, not via its header condition. • Accidental infinite loops usually come from a condition variable that's never updated, or an update that never reaches the stopping condition (watch unsigned counters compared with >=).

The Core Control-Flow Shapes

The switch falls through case 'A' into case 'B' intentionally (no break between them) so both grades share the same message. for is chosen for a known bound; while for an unknown, condition-driven count; do-while when at least one run is required.

// if / else if / else
if (score >= 90) {
    grade = 'A';
} else if (score >= 80) {
    grade = 'B';
} else {
    grade = 'F';
}

// switch with fallthrough guarded by break
switch (grade) {
    case 'A':
    case 'B':
        printf("Passing\n"); // intentional fallthrough from 'A' to 'B'
        break;
    default:
        printf("Needs improvement\n");
        break;
}

// for / while / do-while
for (int i = 0; i < 5; i++) { /* known count */ }
while (hasMoreInput()) { /* unknown count, may run zero times */ }
do { attempt(); } while (shouldRetry()); // runs at least once

Code Examples

Cases 6 and 7 intentionally share one body by omitting a break between them, falling through from case 6 into case 7's statements — this is the correct, idiomatic way to group multiple case values, always paired with a real break at the end. Expected result: Weekend Weekday

The while loop's body never executes because its condition is already false on the first check; the do-while loop's body executes exactly once even though its condition is also false after the first pass, demonstrating the guaranteed-at-least-once behavior that distinguishes it from while. Expected result: count 1 count 2 count 3 attempts = 1

Each goto jumps forward to exactly the cleanup steps needed for what was already allocated, and execution falls through the remaining labels on the way out — every resource is released exactly once from a single place, regardless of which point the function failed at. This is the one broadly accepted use of goto in C.

Common mistake: Using goto to jump backward or sideways for ordinary looping/branching (instead of this single-exit-cleanup pattern) produces exactly the tangled, hard-to-follow control flow that gives goto its bad reputation — always prefer for/while/if for anything that isn't this cleanup case.

#include <stdio.h>

void describeDay(int day) {
    switch (day) {
        case 6:
        case 7:
            printf("Weekend\n");
            break;
        default:
            printf("Weekday\n");
            break;
    }
}

int main(void) {
    describeDay(6); // Weekend
    describeDay(3); // Weekday
    return 0;
}
#include <stdio.h>

int main(void) {
    // Known bound: for
    for (int i = 1; i <= 3; i++) {
        printf("count %d\n", i);
    }

    // Unknown bound, may run zero times: while
    int value = 10;
    while (value > 100) {  // false immediately
        printf("never runs\n");
        value -= 1;
    }

    // Must run at least once: do-while
    int attempts = 0;
    do {
        attempts++;
    } while (attempts < 1); // condition false after first pass, but body already ran

    printf("attempts = %d\n", attempts);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>

int processFile(const char *path) {
    int result = -1;

    FILE *file = fopen(path, "r");
    if (file == NULL) {
        return -1; // nothing acquired yet, plain return is fine
    }

    char *buffer = malloc(1024);
    if (buffer == NULL) {
        goto cleanup_file; // free only what was actually acquired
    }

    int *scratch = malloc(sizeof(int) * 100);
    if (scratch == NULL) {
        goto cleanup_buffer;
    }

    // ... real work using file, buffer, scratch ...
    result = 0; // success

    free(scratch);
cleanup_buffer:
    free(buffer);
cleanup_file:
    fclose(file);
    return result;
}

goto Cleanup Pattern: Fall-Through Exit

Every arrow eventually funnels through the same cleanup labels, guaranteeing each resource is freed exactly once no matter where the function bailed out.

fopen ──fail──▶ return -1
    │ ok
    ▼
  malloc(buffer) ──fail──▶ goto cleanup_file ─────────────┐
    │ ok                                                    │
    ▼                                                        │
  malloc(scratch) ──fail──▶ goto cleanup_buffer ──┐          │
    │ ok                                            │          │
    ▼                                                ▼          ▼
  do work                                    free(buffer) ◀── free(scratch)
    │                                                │
    ▼                                                ▼
  free(scratch) ──────────────────────────▶  fclose(file) ──▶ return

Important Rules

Always brace every if/else body, even single statements, to prevent a common class of bug.

Always terminate a switch case with break unless the fallthrough is intentional and commented.

Pick for when the iteration count is known, while when it isn't and may be zero, do-while when at least one pass is required.

Prefer return/break/continue over goto for all ordinary control flow.

Reserve goto strictly for the single-exit cleanup pattern in low-level C functions; never use it for general branching.

Event Loops as Intentional Infinite Loops

A typical Objective-C app's main run loop, underneath Cocoa's abstractions, is fundamentally a C-style for (;;) or while (1) loop that repeatedly waits for and dispatches events, exiting only when the application is told to terminate — the same intentional-infinite-loop pattern covered in this lesson, just wrapped in a higher-level API.

switch (x) { case 1: doA(); case 2: doB(); break; } switch (x) { case 1: doA(); break; case 2: doB(); break; }

Missing break after case 1 causes execution to fall through into case 2's statements even when x is 1, running both doA() and doB() instead of just doA().

for (unsigned int i = count - 1; i >= 0; i--) { process(i); } // count could be 0 for (unsigned int i = count; i-- > 0;) { process(i); }

An unsigned counter compared with >= 0 is always true (unsigned values can never be negative), so decrementing past 0 wraps around to a huge positive number instead of stopping, producing an effectively infinite loop.

if (x > 0) doThing(); doOtherThing(); // programmer thought this was inside the if if (x > 0) { doThing(); doOtherThing(); }

Without braces, only the single statement immediately after the if is conditional; doOtherThing() actually runs unconditionally every time, which is invisible from indentation alone.

What happens if a switch case's statements are not followed by break? A. A compile error occurs B. Execution falls through and also runs the next case's statements C. The switch silently exits at that point D. The default case runs instead

B. Execution falls through and also runs the next case's statements — C's switch falls through by default: without an explicit break, control continues into the following case's statements regardless of whether that case's value actually matched.

Which loop guarantees its body runs at least once, even if the condition is false from the start? A. for B. while C. do-while D. None of them guarantee this

C. do-while — do-while checks its condition after executing the body, so the body always runs at least one time before the condition is ever evaluated.

What is the generally accepted legitimate use of goto in C? A. Replacing for loops for better performance B. Jumping backward to repeat a block of code C. A single designated cleanup/exit label that releases resources acquired earlier on any failure path D. Skipping over an if/else chain to save typing

C. A single designated cleanup/exit label that releases resources acquired earlier on any failure path — The widely accepted use is a forward jump to a shared cleanup label at the end of a function, guaranteeing every previously acquired resource is released exactly once regardless of which failure point was hit — general looping or branching with goto is discouraged.

Inside a loop nested within another loop, what does break do? A. Exits both the inner and outer loop B. Exits only the innermost loop it is directly inside C. Exits the entire function D. Skips to the next iteration of the outer loop only

B. Exits only the innermost loop it is directly inside — break only affects the single loop (or switch) it is lexically inside; it has no effect on any loop enclosing that one, which continues normally.

Write a C function int findFirstNegative(int arr[], int n) that returns the index of the first negative number in arr, or -1 if none exists, using a for loop with an early return. Then write a switch statement that maps an integer 1-7 to a day-name string using default for anything out of range.

return inside the loop body exits both the loop and the function in one step — no need for a separate 'found' flag.