Conditions and loops

A program that only runs straight ahead cannot do much. In C++ a condition chooses between two paths with if, a loop repeats a block, and the two combine into almost every algorithm in the course: a loop over a container with a condition inside.

This lesson builds each form as a complete program and runs it, then breaks a switch on purpose, because a fallthrough is the mistake that looks correct and prints the wrong number.

if, else if, else

An if takes a condition and runs a block when it is true. The else branch runs otherwise, and a chain of else if tests the cases in order, stopping at the first match. Later conditions are not evaluated once one has matched, which matters when they have side effects.

The condition is converted to bool. An int is true when it is not zero, a pointer is true when it is not null, and a std::string is not converted at all: use !s.empty(). Being explicit about the test is part of writing readable conditions.

The program below grades three scores with one chain. The output shows the first match for each: 95 is an A, 72 a B, and 40 falls through to the else branch.

#include <iostream> int main() { int scores[]{95, 72, 40}; for (int score : scores) { std::cout << score << ": "; if (score >= 90) { std::cout << "A\n"; } else if (score >= 70) { std::cout << "B\n"; } else { std::cout << "C\n"; } } }

g++ -std=c++23 -Wall -Wextra if.cpp -o if_demo
./if_demo

95: A
72: B
40: C

switch

A switch compares one integer or enum value against a list of constants and jumps to the matching case. It reads better than a chain of else if when every branch tests the same value against a small set.

Each case needs a break, or execution continues into the next case. The default label catches every value that no case matched, and putting it last is a convention rather than a requirement.

The dispatch below turns three command letters into three messages, including the unknown one. A switch is not faster than an if chain in any way that matters here; it is a shape that makes the set of handled values visible at a glance.

#include <iostream> int main() { const char commands[]{'a', 'd', 'x'}; for (char command : commands) { std::cout << command << ": "; switch (command) { case 'a': std::cout << "add\n"; break; case 'd': std::cout << "delete\n"; break; default: std::cout << "unknown\n"; break; } } }

g++ -std=c++23 -Wall -Wextra switch.cpp -o switch_demo
./switch_demo

a: add
d: delete
x: unknown

for

A for loop puts the counter, the condition and the step in one line. The three parts run in a fixed order: initialise once, test before every iteration, step after every iteration. A loop whose condition is false at the start never runs its body.

Use ++i rather than i++ in the step: for an int the two are identical, and for an iterator the prefix form does less work. The habit costs nothing and matters once the loop walks a container.

The program sums 1 to 10 and then counts down from 3. The body changes total but never the counter, which is what keeps the loop finite and easy to read.

#include <iostream> int main() { int total = 0; for (int i = 1; i <= 10; ++i) { total += i; } std::cout << "total: " << total << '\n'; for (int i = 3; i > 0; --i) { std::cout << i << ' '; } std::cout << "go\n"; }

g++ -std=c++23 -Wall -Wextra for.cpp -o for_demo
./for_demo

total: 55
3 2 1 go

while, do-while, break and continue

A while loop repeats as long as its condition holds, and the condition is checked before the body, so zero iterations are possible. A do-while checks after the body, so the body runs at least once; that is the right shape for a retry or a menu.

break leaves the loop immediately, and continue skips the rest of the body and starts the next iteration. Both work inside a for loop as well, and both are easier to read than a flag that the condition tests.

The first loop finds the first power of 3 above 100, the second counts three attempts, and the third counts the odd numbers below 9 while showing where break and continue leave the iteration.

#include <iostream> int main() { int value = 1; while (value < 100) { value *= 3; } std::cout << "first power of 3 above 100: " << value << '\n'; int attempts = 0; do { ++attempts; } while (attempts < 3); std::cout << "attempts: " << attempts << '\n'; int odds = 0; for (int i = 0; i < 10; ++i) { if (i % 2 == 0) { continue; } if (i == 9) { break; } ++odds; } std::cout << "odd numbers below 9: " << odds << '\n'; }

g++ -std=c++23 -Wall -Wextra while.cpp -o while_demo
./while_demo

first power of 3 above 100: 243
attempts: 3
odd numbers below 9: 4

The missing break

A case without break falls into the next one. Sometimes that is intended: several labels share one body. When it is not intended, the code looks right and the value is wrong, which is why GCC added -Wimplicit-fallthrough and why -Wextra turns it on.

The function below gives a 5 percent discount for one item and 20 percent for ten. The case for 1 has no break, so it runs the body of case 10 as well, and one item is discounted by 20 percent too.

The warning names the line that may fall through, and the fix is a break after the first assignment, or [[fallthrough]] when the fall is intended. The attribute documents the decision for the next reader.

#include <iostream> int discount(int items) { int percent = 0; switch (items) { case 1: percent = 5; case 10: percent = 20; break; default: percent = 0; } return percent; } int main() { std::cout << discount(1) << ' ' << discount(10) << '\n'; }

g++ -std=c++23 -Wall -Wextra fallthrough.cpp -o fallthrough
./fallthrough

fallthrough.cpp: In function 'int discount(int)':
fallthrough.cpp:8:17: warning: this statement may fall through [-Wimplicit-fallthrough=]
8 | percent = 5;
| ~~~~~~~~^~~
fallthrough.cpp:9:5: note: here
9 | case 10:
| ^~~~
20 20

Exercises

  1. Print the numbers from 1 to 20, replacing multiples of 3 with fizz and multiples of 5 with buzz, and multiples of both with fizzbuzz.
  2. Rewrite the grade chain as a switch over score / 10. Which values need a default, and which cases can share a body?
  3. Add a [[fallthrough]] to the discount function and check that the warning disappears without changing the result.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. How does an if/else if/else chain choose a branch?

    Answer

    It runs the first branch whose condition is true

    The chain is exclusive, and the else is the fallback when all the tests fail.

  2. What happens to a switch case without a break?

    Answer

    Execution continues into the next case

    A case is a label, not a block, and the fallthrough is legal unless the compiler warns about it.

  3. What are the three parts of a for loop header?

    Answer

    Initialisation, condition and step

    The three expressions run before the loop, before each iteration and after each iteration.

  4. What is the difference between while and do-while?

    Answer

    do-while checks the condition after the body

    The do-while form always runs the body at least once.

  5. What does this program print? #include <iostream> char grade(int score) { return score >= 90 ? 'A' : score >= 70 ? 'B' : 'C'; } int main() { std::cout << "95: " << grade(95) << ", 72: " << grade(72) << ", 40: " << grade(40) << '\n'; }

    Answer

    95: A, 72: B, 40: C

    Each score falls in its band, and the conditional chain returns A, B and C.

  6. What does this program print? #include <iostream> int main() { int total = 0; for (int i = 1; i <= 10; ++i) total += i; std::cout << "total: " << total << '\n'; for (int i = 3; i >= 1; --i) std::cout << i << ' '; std::cout << "go\n"; }

    Answer

    total: 55 / 3 2 1 go

    The first loop sums 1 to 10, and the second counts down from 3 and prints the word.

  7. What does this program print? #include <iostream> int main() { int power = 1; while (power <= 100) power *= 3; std::cout << power << '\n'; }

    Answer

    243

    The loop multiplies by three until the value passes 100, and 243 is the first value above it.

  8. What does this program print? #include <iostream> int main() { for (char c : {'a', 'd', 'x'}) { switch (c) { case 'a': std::cout << "a: add, "; break; case 'd': std::cout << "d: delete, "; break; default: std::cout << "x: unknown"; break; } } }

    Answer

    a: add, d: delete, x: unknown

    Each letter matches its case, and the last one reaches the default.

  9. What does the compiler warn about in the discount switch?

    Answer

    this statement may fall through

    The case assigns a value and continues into the next case, and the note shows where execution lands.

  10. What does this program print? #include <iostream> int discount(int level) { int percent = 0; switch (level) { case 5: percent = 10; case 10: percent = 20; } return percent; } int main() { std::cout << discount(5) << ' ' << discount(10); }

    Answer

    20 20

    The case 5 has no break, so it falls into case 10 and both calls return 20; the compiler warns about the fallthrough.

The code on this page was run with g++ 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev