Skip to content

C lesson 3 of 8

Control Flow in C: if, switch and Loops

How C chooses between paths and repeats work - if and else if, why any nonzero value counts as true, switch and deliberate fall-through, while, do-while and for, and steering a loop with break and continue.

Published · Every example on this page was run before it was published.

A program that runs the same statements in the same order every time is a calculator with one button. Control flow is everything that lets a program take one path rather than another, and repeat work until it is done. C's set of tools here is small and almost every later language borrowed it, so learning it once pays off repeatedly: two ways to choose, three ways to loop, and two keywords for steering a loop from the inside.

if, else if, else

An if runs its block when its condition is true, and an optional else runs when it is not. Chain them with else if to test a series of possibilities in order.

C
#include <stdio.h>

int main(void) {
    int temperature = 31;

    if (temperature > 35) {
        printf("Heat warning\n");
    } else if (temperature > 28) {
        printf("Warm\n");
    } else {
        printf("Comfortable\n");
    }
    return 0;
}
Output
Warm

Order matters in a chain: conditions are tested top to bottom and the first true one wins, after which the rest are skipped entirely. 31 is not greater than 35, it is greater than 28, so the second block runs and the else never gets a look. Had the first two tests been swapped, every warm reading would have matched > 28 first and the heat warning would have been unreachable.

The braces are optional when a branch is a single statement, but leave them in. A great deal of real damage has come from adding a second line to a braceless if and not noticing that only the first line is still controlled by the condition.

There Is No Boolean Type to Speak Of

C has no separate true/false type at its core. A condition is just an integer expression, and the rule is simply: zero is false, anything else is true.

C
#include <stdio.h>

int main(void) {
    int count = 0;

    if (count) {
        printf("count is nonzero\n");
    } else {
        printf("count is zero\n");
    }

    printf("%d\n", 5 > 3);
    printf("%d\n", 5 < 3);
    printf("%d\n", 3 == 3);
    return 0;
}
Output
count is zero
1
0
1

Note the second half of that output. The comparison operators do not merely steer an if; they produce a value, 1 for true and 0 for false, that you can print, store or add up. That makes matches += (name[i] == target) a perfectly ordinary way to count matches.

The comparison operators are == equal, != not equal, <, >, <=, >=. The logical operators are && for and, || for or, and ! for not. C99 also added a real bool type in <stdbool.h>, with true and false spelled out, and it is worth using for readability - but underneath it is still an integer that is zero or not.

Short-Circuit Evaluation

&& and || evaluate their left side first and skip the right side entirely when the answer is already settled. false && anything is false, and true || anything is true, so there is nothing left to compute. This is not an optimisation detail you can ignore; it is a guarantee you can build on.

C
#include <stdio.h>

int main(void) {
    int stock = 0;
    int price = 12;

    if (stock > 0 && price / stock < 5) {
        printf("cheap and in stock\n");
    } else {
        printf("not available\n");
    }

    if (stock == 0 || price > 100) {
        printf("at least one condition held\n");
    }
    return 0;
}
Output
not available
at least one condition held

The first condition contains price / stock, a division by zero - which in C is undefined behaviour and must never be allowed to happen. It does not happen here, because stock > 0 is false and && stops there. Guarding a dangerous operation behind a test on the same line is one of the most common uses of && in C, and it only works because the order of evaluation is guaranteed. Put the two halves the wrong way round and the guard is worthless.

The = Versus == Trap

In C an assignment is an expression whose value is the value assigned. That makes this legal:

C
int lives = 0;

if (lives = 3) {
    /* This block runs, and lives is now 3. */
}

There is no syntax error and nothing stops at run time. The condition assigns 3 to lives, the whole expression evaluates to 3, and 3 is nonzero, so the block runs - every time, whatever lives held before. Most compilers warn about an assignment used as a condition, and that warning is worth reading rather than dismissing. Some programmers write constants on the left (if (3 == lives)) so that a slipped keystroke becomes an error rather than a silent bug.

The Conditional Operator

For choosing between two values rather than two blocks, C has a three-part operator: condition ? value_if_true : value_if_false.

C
#include <stdio.h>

int main(void) {
    int items = 1;

    printf("%d %s\n", items, items == 1 ? "crate" : "crates");

    items = 4;
    printf("%d %s\n", items, items == 1 ? "crate" : "crates");

    int a = 17;
    int b = 9;
    printf("larger: %d\n", a > b ? a : b);
    return 0;
}
Output
1 crate
4 crates
larger: 17

It is worth using where it keeps an expression readable, as in pluralising a word, and worth avoiding when nested, because nested conditionals read far worse than the if chain they replace.

switch

When you are comparing one integer or character against a list of constant possibilities, switch says so more clearly than a chain of ifs.

C
#include <stdio.h>

int main(void) {
    char grade = 'B';

    switch (grade) {
        case 'A':
            printf("Excellent\n");
            break;
        case 'B':
            printf("Good\n");
            break;
        case 'C':
            printf("Adequate\n");
            break;
        default:
            printf("Unrecognised grade\n");
            break;
    }
    return 0;
}
Output
Good

Control jumps straight to the matching case label, runs from there, and break exits the switch. default catches everything unmatched; it is optional, but leaving it out means an unexpected value does nothing at all, silently.

Each case label must be a constant the compiler can work out for itself - a literal, a character, or a name from an enum. You cannot write case x: for a variable x, and you cannot write a range or a comparison. That is the price of switch being a jump rather than a series of tests.

Fall-Through, Deliberate and Accidental

A case is a label, not a block. Without a break, execution runs straight on into the next case. Used on purpose, that lets several labels share one body:

C
#include <stdio.h>

int main(void) {
    int day = 6;

    switch (day) {
        case 6:
        case 7:
            printf("Weekend\n");
            break;
        default:
            printf("Weekday\n");
            break;
    }
    return 0;
}
Output
Weekend

Used by accident, it produces output nobody asked for:

C
#include <stdio.h>

int main(void) {
    int level = 1;

    switch (level) {
        case 1:
            printf("one\n");
        case 2:
            printf("two\n");
        case 3:
            printf("three\n");
            break;
    }
    return 0;
}
Output
one
two
three

level is 1, so control enters at case 1: - and then simply keeps going, because nothing tells it to stop. Three lines printed for one matching case. If you ever intend to fall through, say so in a comment at the point it happens, so the next reader knows it was a decision.

while

A while loop tests its condition before each pass and stops the first time the condition is false.

C
#include <stdio.h>

int main(void) {
    int remaining = 3;

    while (remaining > 0) {
        printf("%d to go\n", remaining);
        remaining--;
    }
    printf("Done\n");
    return 0;
}
Output
3 to go
2 to go
1 to go
Done

remaining-- is the decrement operator; it subtracts one. remaining++ adds one, and both have a long history of showing up in loop bodies. Something inside the loop must move the condition toward being false, or the loop never ends - forget the remaining-- and this program prints 3 to go until it is killed.

do-while

A do-while puts the test at the bottom, so the body always runs at least once. Note the semicolon after the closing while, which is required here and only here.

C
#include <stdio.h>

int main(void) {
    int tries = 0;

    do {
        tries++;
        printf("Attempt %d\n", tries);
    } while (tries < 2);

    int zero = 0;

    while (zero > 0) {
        printf("A while loop never enters here.\n");
    }

    do {
        printf("A do-while body runs before the test is seen.\n");
    } while (zero > 0);

    return 0;
}
Output
Attempt 1
Attempt 2
A do-while body runs before the test is seen.

The while in the middle printed nothing, because its condition was already false. The do-while underneath printed once with the very same condition. Reach for it when the work has to happen before you can possibly know whether to repeat it - reading a value, then deciding whether it was acceptable.

for

A for loop gathers the three parts of a counting loop into one line: an initialisation that runs once, a condition tested before every pass, and an update that runs after every pass.

C
#include <stdio.h>

int main(void) {
    for (int i = 1; i <= 5; i++) {
        printf("%d squared is %d\n", i, i * i);
    }
    return 0;
}
Output
1 squared is 1
2 squared is 4
3 squared is 9
4 squared is 16
5 squared is 25

Declaring the counter inside the for - int i = 1 - keeps it local to the loop, so it does not exist afterwards and cannot collide with anything else. That has been legal since C99 and is the style to prefer.

Counting from zero with a < condition is overwhelmingly the common form in C, because it matches how arrays are indexed: for (int i = 0; i < n; i++) visits exactly n items, numbered 0 to n - 1. Getting comfortable with that shape now will save you a great many off-by-one errors later.

Loops nest, and the inner one runs in full for every single pass of the outer one:

C
#include <stdio.h>

int main(void) {
    for (int row = 1; row <= 3; row++) {
        for (int col = 1; col <= 3; col++) {
            printf("%d", row * col);
            if (col < 3) {
                printf(",");
            }
        }
        printf("\n");
    }
    return 0;
}
Output
1,2,3
2,4,6
3,6,9

Nine numbers from three passes of the outer loop. The if inside prints a comma between values but not after the last, and the printf("\n") sits in the outer loop, which is what ends each row.

break and continue

break leaves the enclosing loop immediately. continue abandons the rest of the current pass and jumps to the loop's update step, carrying on with the next one.

C
#include <stdio.h>

int main(void) {
    for (int n = 1; n <= 10; n++) {
        if (n % 3 == 0) {
            continue;
        }
        if (n > 7) {
            printf("stopping at %d\n", n);
            break;
        }
        printf("kept %d\n", n);
    }
    return 0;
}
Output
kept 1
kept 2
kept 4
kept 5
kept 7
stopping at 8

3 and 6 were skipped by the continue before they could be printed. 9 was never reached, because 8 failed the second test and break ended the loop there - the loop's own condition said n <= 10, but break does not care about that.

One warning worth knowing in advance: in a nested loop, break leaves only the loop it is written in, not all of them. And in a switch inside a loop, break belongs to the switch, which is a genuinely easy mistake to make.

A Worked Example

Everything above, in one program: a loop with a continue filter, a running total, and a final decision about how to report it.

C
#include <stdio.h>

int main(void) {
    int total = 0;
    int count = 0;

    for (int n = 1; n <= 20; n++) {
        if (n % 4 != 0) {
            continue;
        }
        count++;
        total += n;
        printf("hit %d (running total %d)\n", count, total);
    }

    if (count == 0) {
        printf("No multiples found.\n");
    } else {
        printf("%d multiples of 4, averaging %.1f\n", count, (double) total / count);
    }
    return 0;
}
Output
hit 1 (running total 4)
hit 2 (running total 12)
hit 3 (running total 24)
hit 4 (running total 40)
hit 5 (running total 60)
5 multiples of 4, averaging 12.0

The continue does the filtering, so the body below it only ever sees multiples of four: 4, 8, 12, 16 and 20. count++ and total += n accumulate as the loop goes, and total += n is shorthand for total = total + n.

The if at the end is not decoration. Had the loop found nothing, count would be zero and (double) total / count would be a division by zero - undefined behaviour, not a neat "infinity". The guard makes sure that expression is only ever reached when count is at least one. The cast, as in the previous lesson, is what stops 60 / 5 being an integer division; here it happens to come out exactly, but the cast is what makes %.1f meaningful in general.

Common Mistakes

A semicolon after an if or a for header. if (x > 0); is an if whose body is the empty statement. The block that follows then runs unconditionally, and the code looks completely correct. while (x > 0); is worse: an empty loop that never ends.

Using = where == was meant. Covered above, and worth repeating because it compiles. If a condition seems to be true no matter what, look for a single equals sign in it.

Comparing three things at once. if (0 <= x <= 10) does not mean what it appears to. C evaluates 0 <= x first, getting 0 or 1, and then compares that with 10 - which is always true. Write if (x >= 0 && x <= 10).

Forgetting break in a switch. The accidental fall-through above. Every case needs a break unless you deliberately want the next one to run too.

Changing the loop counter in two places. A for loop that also does i++ in its body advances twice per pass. If a loop is skipping every other item, this is the first thing to check.

An off-by-one boundary. for (int i = 0; i <= n; i++) runs n + 1 times, which is one too many for walking an array of n items. Use i < n with a start of 0, or i <= n with a start of 1, and be deliberate about which.

continue in a while loop skipping the update. In a for loop, continue still runs the update step. In a while loop there is no update step, so if the counter is incremented at the bottom of the body, continue jumps straight past it and the loop never ends. Increment before the continue, or use a for.

Next Steps

In the C playground, write a loop from 1 to 30 that prints only the numbers divisible by 3, then add a break that stops it once the running total passes 100. Then write the same thing as a while loop, and notice which parts you had to move.

The next lesson is functions: how to give a block of C a name and a set of inputs, why C wants to know about a function before you call it, and what it means that arguments are copied rather than shared.

Write it yourself

Reading about code and writing it are different skills. These exercises practise exactly what this lesson covered; they run in this tab and need no account.