Control flow#

C’s control surface is small and C-shaped. if / else, while, do while, for, switch / case, break / continue / return / goto. There is no exception flow; the operator either returns a code or uses setjmp / longjmp (see Errors) for the rare cases where unwinding by hand is impossible.

For break inside switch and the fall-through trap, read the switch section carefully.

Conditional#

if / else if / else. The condition is any expression; non-zero is true, zero is false.

        flowchart TD
    A([start]) --> B{x > 0?}
    B -->|true| C[positive]
    B -->|false| D{x == 0?}
    D -->|true| E[zero]
    D -->|false| F[negative]
    C --> Z([end])
    E --> Z
    F --> Z
    
if (x > 0) {
    positive();
} else if (x == 0) {
    zero();
} else {
    negative();
}

C has the ternary expression for inline branches.

int abs = (x < 0) ? -x : x;

Useful when the branch fits one expression.

const char *label = (n % 2 == 0) ? "even" : "odd";

The operator always braces single-statement blocks; if (x) a(); plus a later “let me add a second line” is the original Apple goto fail family of bug.

Loops#

For#

        flowchart TD
    A([init]) --> B{cond?}
    B -->|true| C[body]
    C --> D[step]
    D --> B
    B -->|false| E([end])
    

C-style: init, condition, step.

for (int i = 0; i < 10; i++) {
    printf("%d\n", i);
}

Pointer-walk over a linked list.

for (node_t *p = head; p != NULL; p = p->next) {
    visit(p);
}

While#

        flowchart TD
    A([start]) --> B{cond?}
    B -->|true| C[body]
    C --> B
    B -->|false| D([end])
    

Test-first.

while (queue_len(q) > 0) {
    process(queue_pop(q));
}

Do-while#

        flowchart TD
    A([start]) --> B[body]
    B --> C{cond?}
    C -->|true| B
    C -->|false| D([end])
    

Test-after; runs the body at least once.

do {
    line = read_line(stdin);
    handle(line);
} while (strcmp(line, "quit") != 0);

C has no range-style iteration; write the counter or pointer walk explicitly.

Switch#

switch dispatches on an integer expression (int, char, enum). Cases fall through by default; the operator writes break on every case to stop that.

        flowchart TD
    A([switch state]) --> B{case IDLE?}
    B -->|yes| C[start]
    B -->|no| D{case RUNNING?}
    D -->|yes| E[step]
    D -->|no| F{case DONE?}
    F -->|yes| G[cleanup]
    F -->|no| H[default]
    C --> Z([break out])
    E --> Z
    G --> Z
    H --> Z
    
switch (state) {
case STATE_IDLE:
    start();
    break;
case STATE_RUNNING:
    step();
    break;
case STATE_DONE:
    cleanup();
    break;
default:
    fprintf(stderr, "bad state: %d\n", state);
    return -1;
}

When fall-through is deliberate, the operator marks it with a comment (or with the C23 [[fallthrough]] attribute) so -Wimplicit-fallthrough does not complain.

switch (kind) {
case A:
case B:
    handle_ab();
    break;
case C:
    prepare();
    [[fallthrough]];           /* C23 attribute */
case D:
    finish();
    break;
}

Case labels accept only integer constants (no ranges in standard C; GCC extension allows 1 ... 5).

Jumps#

Keyword

Effect

break

Exit the innermost loop or switch.

continue

Skip the rest of the loop body, jump to the next iteration.

return

Exit the current function with a value (or void).

goto label

Jump to a named label in the same function. Common use is cleanup-on-failure.

The cleanup pattern.

int run(void) {
    FILE *f = fopen(path, "r");
    if (!f) return -1;

    char *buf = malloc(SIZE);
    if (!buf) { fclose(f); return -1; }

    if (process(f, buf) < 0) goto err;
    /* success path */
    free(buf);
    fclose(f);
    return 0;

err:
    free(buf);
    fclose(f);
    return -1;
}

goto to a cleanup label avoids nested if ladders and is the standard kernel-style pattern in C.

References#