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.
#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;
}WarmOrder 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.
#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;
}count is zero
1
0
1Note 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.
#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;
}not available
at least one condition heldThe 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:
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.
#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;
}1 crate
4 crates
larger: 17It 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.
#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;
}GoodControl 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:
#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;
}WeekendUsed by accident, it produces output nobody asked for:
#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;
}one
two
threelevel 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.
#include <stdio.h>
int main(void) {
int remaining = 3;
while (remaining > 0) {
printf("%d to go\n", remaining);
remaining--;
}
printf("Done\n");
return 0;
}3 to go
2 to go
1 to go
Doneremaining-- 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.
#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;
}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.
#include <stdio.h>
int main(void) {
for (int i = 1; i <= 5; i++) {
printf("%d squared is %d\n", i, i * i);
}
return 0;
}1 squared is 1
2 squared is 4
3 squared is 9
4 squared is 16
5 squared is 25Declaring 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:
#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;
}1,2,3
2,4,6
3,6,9Nine 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.
#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;
}kept 1
kept 2
kept 4
kept 5
kept 7
stopping at 83 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.
#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;
}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.0The 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.
- Umbrella or NotA rain report card that reads the forecast for you and signs off with a straight answer — take the umbrella, or leave it at home.
- CountdownA launch sequence that counts itself down from five to one, announces liftoff and draws a small rocket, with the whole countdown written as one line that runs five times.
- The Clapping GameA count from one to twenty where every third number is replaced by a clap, so the beat is visible straight down the screen.