Control Flow

This section documents C23 (ISO/IEC 9899:2024), per ISO/IEC JTC1/SC22/WG14’s freely available working draft N3220, which WG14 documents as differing from the published standard only editorially — the reference these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against the WG14 draft and cppreference.com’s C reference before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

C has a small, entirely statement-based set of control-flow constructs — there are no expression-level if or match forms, and the only expression that chooses between values is the conditional operator ?:. What C does have is switch fall-through and goto, both of which are more useful than their reputation suggests.

if / else

Any scalar expression works as a condition — integers, floating-point values and pointers are all compared against zero implicitly:

#include <stdio.h>
#include <string.h>

int main(void)
{
    int n = 7;

    if (n > 10) {
        puts("big");
    } else if (n > 5) {
        puts("medium");
    } else {
        puts("small");
    }

    const char *name = "abc";
    if (name != nullptr && strlen(name) > 0) {       // explicit is better than if (name)
        puts(name);
    }
    return 0;
}

Two habits prevent the classic bugs:

  • Always brace the body, even for one statement. The unbraced form is how goto fail shipped, and it makes every later edit riskier.

  • Compare explicitly — if (p != nullptr), if (count != 0). if (p) is idiomatic and fine, but explicit comparisons stop if (x = 1) typos from looking plausible.

An else binds to the nearest unmatched if — the "dangling else" — which braces make moot:

#include <stdio.h>

int main(void)
{
    int a = 0, b = 1;

    if (a) {
        if (b) {
            puts("both");
        }
    } else {
        puts("not a");          // unambiguous, because of the braces
    }
    return 0;
}

switch

A switch selects on an integer expression (including char and enum, but never a float, a string or a range), comparing it against constant case labels:

#include <stdio.h>

enum Level { LEVEL_DEBUG, LEVEL_INFO, LEVEL_WARN, LEVEL_ERROR };

static const char *level_name(enum Level level)
{
    switch (level) {
    case LEVEL_DEBUG:
        return "DEBUG";
    case LEVEL_INFO:
        return "INFO";
    case LEVEL_WARN:
        return "WARN";
    case LEVEL_ERROR:
        return "ERROR";
    default:
        return "UNKNOWN";
    }
}

int main(void)
{
    printf("%s %s\n", level_name(LEVEL_WARN), level_name((enum Level)42));
    return 0;
}

Fall-Through

Control falls from one case into the next unless something stops it. That is occasionally what you want and usually a bug, so C23 gives you a way to say which:

#include <stdio.h>

static int char_class(char c)
{
    int flags = 0;

    switch (c) {
    case 'a':
    case 'e':
    case 'i':
    case 'o':
    case 'u':
        flags |= 1;             // deliberate: labels stacked with no statements between
        break;

    case 'Y':
        flags |= 4;             // "sometimes a vowel"
        [[fallthrough]];        // C23: deliberate, and silences -Wimplicit-fallthrough
    case 'y':
        flags |= 1;
        break;

    default:
        flags |= 2;
        break;
    }
    return flags;
}

int main(void)
{
    printf("%d %d %d %d\n", char_class('a'), char_class('Y'), char_class('y'), char_class('z'));
    return 0;
}

Details that matter:

  • Stacked labels with nothing between them (case 'a': case 'e':) are not fall-through and never warn.

  • ; is a statement and needs its semicolon. Before C23, use __attribute__((fallthrough)) or a /* fall through */ comment that GCC/Clang recognize.

  • Always write a default — even default: break; — so a new enumerator does not silently do nothing. With -Wswitch-enum, omitting default for an enum switch is instead how you get told about the new enumerator, which some codebases prefer.

  • A declaration directly after a case label needs a block: case 1: { int x = f(); …​ }.

Loops

C has three loop statements. The difference that matters is when the condition is tested: for and while test before the body (so they may run zero times), while do-while tests after it (so it always runs at least once).

flowchart TB subgraph FOR["for (init; cond; incr) body"] direction TB f0([init - once]) --> f1{cond} f1 -->|true| f2[body] f2 --> f3[incr] f3 --> f1 f1 -->|false| f4([exit]) end subgraph WHILE["while (cond) body"] direction TB w1{cond} -->|true| w2[body] w2 --> w1 w1 -->|false| w3([exit]) end subgraph DOWHILE["do body while (cond);"] direction TB d1[body - runs at least once] --> d2{cond} d2 -->|true| d1 d2 -->|false| d3([exit]) end

for

#include <stddef.h>
#include <stdio.h>

int main(void)
{
    int values[5] = { 1, 2, 3, 4, 5 };
    size_t count = sizeof values / sizeof values[0];

    for (size_t i = 0; i < count; ++i) {        // C99: declare the counter in the loop
        printf("%zu:%d ", i, values[i]);
    }
    putchar('\n');

    for (int *p = values; p != values + count; ++p) {   // pointer walk
        printf("%d ", *p);
    }
    putchar('\n');

    for (int i = 0, j = 4; i < j; ++i, --j) {   // comma operator in both clauses
        printf("%d-%d ", values[i], values[j]);
    }
    putchar('\n');

    for (;;) {                                  // deliberate infinite loop
        break;
    }
    return 0;
}

Declaring the counter in the for statement scopes it to the loop, which is what you almost always want. Use size_t for indices into arrays — comparing a signed int against sizeof is the sign-compare warning from Basic Types and Values.

while and do-while

#include <stdio.h>

int main(void)
{
    int countdown = 3;
    while (countdown > 0) {             // test first: may run zero times
        printf("%d ", countdown--);
    }
    putchar('\n');

    int attempts = 0;
    do {
        ++attempts;                     // body first: always runs at least once
    } while (attempts < 3);

    printf("attempts=%d\n", attempts);
    return 0;
}

The do-while form is worth remembering for two things: input validation loops that must read at least once, and multi-statement macros (do { …​ } while (0)) — see Preprocessor and Macros. Note the mandatory semicolon after while (0).

The idiomatic C read loop relies on assignment being an expression:

#include <stdio.h>

int main(void)
{
    int c;
    while ((c = getchar()) != EOF) {    // note: int, not char -- EOF does not fit in a char
        putchar(c);
    }
    return 0;
}

break and continue

break leaves the innermost loop or switch; continue skips to the next iteration (to the increment clause, in a for):

#include <stdio.h>

int main(void)
{
    for (int i = 0; i < 10; ++i) {
        if (i % 2 == 0) {
            continue;                   // skip even numbers
        }
        if (i > 7) {
            break;                      // stop entirely
        }
        printf("%d ", i);               // 1 3 5 7
    }
    putchar('\n');

    // break inside a switch inside a loop leaves the SWITCH, not the loop:
    for (int i = 0; i < 3; ++i) {
        switch (i) {
        case 1:
            break;                      // leaves the switch; the loop continues
        default:
            printf("%d ", i);
            break;
        }
    }
    putchar('\n');
    return 0;
}

C has no labeled break. To leave two loops at once, use a flag, a function with return, or goto — and goto is the clearest of the three.

goto and Labels

goto jumps to a label in the same function. It cannot jump into the scope of a variable-length array, and jumping over an initialization leaves that object uninitialized.

Its one thoroughly idiomatic use is centralized cleanup, which is how the Linux kernel and most C libraries handle multi-step allocation failure:

#include <stdio.h>
#include <stdlib.h>

static int process(const char *path, size_t n)
{
    int status = -1;

    int *buffer = malloc(n * sizeof *buffer);
    if (buffer == nullptr) {
        goto out;                       // nothing acquired yet
    }

    FILE *f = fopen(path, "rb");
    if (f == nullptr) {
        goto free_buffer;               // release in reverse order of acquisition
    }

    if (fread(buffer, sizeof *buffer, n, f) != n) {
        goto close_file;
    }

    status = 0;                         // success

close_file:
    fclose(f);
free_buffer:
    free(buffer);
out:
    return status;
}

int main(void)
{
    printf("status=%d\n", process("/nonexistent", 16));
    return 0;
}

The alternative — nested if`s or a `free before every return — is what actually causes leaks. Use goto forward only, to cleanup labels named after what they release, and nowhere else.

Escaping nested loops is the other defensible use:

#include <stdio.h>

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

    for (int r = 0; r < 3; ++r) {
        for (int c = 0; c < 3; ++c) {
            if (grid[r][c] == target) {
                printf("found at %d,%d\n", r, c);
                goto found;
            }
        }
    }
    puts("not found");
found:
    return 0;
}

C23 also allows a label at the end of a compound statement (found: }), which previously needed a stray ;.

return

#include <stdio.h>

static int clamp(int value, int low, int high)
{
    if (value < low) {
        return low;                     // early return -- flatter than nested else
    }
    if (value > high) {
        return high;
    }
    return value;
}

static void log_line(const char *msg)
{
    if (msg == nullptr) {
        return;                         // bare return in a void function
    }
    puts(msg);
}

int main(void)
{
    printf("%d %d %d\n", clamp(-5, 0, 10), clamp(5, 0, 10), clamp(50, 0, 10));
    log_line(nullptr);
    log_line("done");
    return 0;
}

return in a non-void function must supply a value (falling off the end and then using the result is undefined) — except in main, where it means return 0. Never return a pointer to a local object: its lifetime ends with the function. See Storage Duration, Scope and Linkage.

See Also

References