Loops in C — practice with the bus ticket machine, number series and a 7-attempt guessing game (and the cent the machine kept overcharging me)
The first version of the ticket machine I wrote for this topic had a lovely loop: it added up the rider’s coins while money was still due. I tried it with the young ticket, €1.05, put in exactly 1.05… and the machine asked me for one more cent:
=== TICKET MACHINE - LINE 12 === Ticket (1 general, 2 young, 3 child, 0 close): 2 Due 1.05 €. Insert money: 1.05 Due 0.01 €. Insert money: 0.01 Ticket issued. Change: 0.00 € Ticket (1 general, 2 young, 3 child, 0 close): 0 --- End of the day --- Riders served: 1 Collected: 1.05 €
It was not a bug in the loop, but in what the loop was adding: 1.05 stored in a float is really 1.0499999…, and when you turn it into cents with (int)(money * 100) the decimals are cut off and you get 104. The loop did the right thing: 1 cent was due, so it kept asking. The lesson repeats in today’s three programs: a loop is only as good as its condition, and the condition depends on the data it looks at being what you think it is.
In the loops in C theory we looked at for, while and do…while one by one. Here we put them together in three complete medium-level programs, all with scanf and with data that looks like the real thing: people typing letters where numbers go, amounts with treacherous decimals and a player who does not guess right the first time:
- The bus ticket machine, which now really serves several riders: a
while (1)with ado…whileand anotherwhileinside. - A series calculator (sum, factorial, Fibonacci and powers) with
forand accumulators. - The number guessing game with 7 attempts, a
forwith two ways out.
Table of Contents
Before you start
It helps to be clear on:
- How a
forruns (initialisation once, condition before every lap, update at the end) and what the variable is worth on exit. - The difference between
while(0 laps possible) anddo…while(at least one). - What
breakandcontinuedo, and thatbreakonly leaves the innermost loop. - That
scanfreturns how many items it read, and that if it fails it leaves what was typed in the input.
If any of these sounds shaky, go back over the theory before carrying on.
Compile each program with gcc -Wall -std=c11 program.c -o program and run it with ./program. If you do not have Fedora at hand, paste it into the site’s C playground: whatever the program reads with scanf goes in the Input tab, one value per line, like in the outputs in this article.
Program 1: the bus ticket machine
The line 12 machine sells three tickets: general (€1.50), young (€1.05, 30 % less) and child (free, under 6). Made-up fares, like in the conditionals topic. Now it has to serve every rider of the day without restarting: it asks for the ticket type (and does not break if someone types a word), charges by adding coins until the price is reached, gives change and, when the driver picks 0, shows how many riders it has served and how much it has collected.
The idea
There are three questions that repeat, and each one is a loop:
- Is there another rider? We do not know how many there will be:
while (1)and abreakwhen the type is 0. - Is the ticket type valid? It has to be asked at least once:
do…while. - Is money still due? While
paid < price, ask for more:while. With the child ticket,0 < 0is false and nothing is asked: exactly the case where awhiledoes 0 laps.
Money is stored in cents (int): 150 instead of 1.50. That way additions and subtractions are exact, which is what a condition like paid < price needs.
Before coding, think about which variables have to survive from one rider to the next (riders, collected) and which ones restart with each rider (paid). The first ones are set up before the while (1); the second ones, inside.
The code
#include <stdio.h>
// Fares made up for the example, in cents so the sums are exact
#define GENERAL 150
#define YOUNG 105 // 30 % off 1.50 €
#define CHILD 0 // under 6: free
int main(void) {
int type; // 1 general, 2 young, 3 child, 0 close
int price; // cents for the chosen ticket
int paid; // cents the rider has put in
int collected = 0; // cents for the whole day
int riders = 0;
float money; // what the rider types, in euros
int c; // to empty the line if they type letters
printf("=== TICKET MACHINE - LINE 12 ===\n");
while (1) { // one lap = one rider
// 1. Ticket type: asked at least once and until it is valid
do {
printf("\nTicket (1 general, 2 young, 3 child, 0 close): ");
if (scanf("%d", &type) != 1) {
while ((c = getchar()) != '\n' && c != EOF); // throws away what was typed
type = -1; // forces another lap
}
if (type < 0 || type > 3) {
printf("Invalid option\n");
}
} while (type < 0 || type > 3);
if (type == 0) {
break; // machine closing: leaves the while (1)
}
switch (type) {
case 1: price = GENERAL; break;
case 2: price = YOUNG; break;
default: price = CHILD; break;
}
// 2. Payment: coins are added until the price is reached
paid = 0;
while (paid < price) {
printf("Due %d.%02d €. Insert money: ", (price - paid) / 100, (price - paid) % 100);
if (scanf("%f", &money) == 1 && money > 0) {
paid += (int)(money * 100 + 0.5f); // euros -> cents, rounding
} else {
while ((c = getchar()) != '\n' && c != EOF);
printf("Invalid amount\n");
}
}
riders++;
collected += price;
printf("Ticket issued. Change: %d.%02d €\n", (paid - price) / 100, (paid - price) % 100);
}
printf("\n--- End of the day ---\n");
printf("Riders served: %d\n", riders);
printf("Collected: %d.%02d €\n", collected / 100, collected % 100);
return 0;
}
The heart of it is the payment lines, which I show again separately:
paid = 0;
while (paid < price) {
printf("Due %d.%02d €. Insert money: ", (price - paid) / 100, (price - paid) % 100);
if (scanf("%f", &money) == 1 && money > 0) {
paid += (int)(money * 100 + 0.5f); // euros -> cents, rounding
} else {
while ((c = getchar()) != '\n' && c != EOF);
printf("Invalid amount\n");
}
}
paid = 0;goes inside thewhile (1), right before the payment. If you put it at the top withcollected, the second rider would inherit what the first one paid.(price - paid) / 100and% 100split euros and cents: with 50 cents due, 50 / 100 = 0 and 50 % 100 = 50, and%02dwrites it as0.50.(int)(money * 100 + 0.5f)rounds instead of cutting: 104.99998 + 0.5 = 105.49998, which is cut to 105. That is the fix for the story.- The condition
scanf(...) == 1 && money > 0rejects letters and negative or zero amounts. Otherwisepaidwould never move forward (or would go backwards).
Output
A general rider paying with two €1 coins, someone typing the word young, a young ticket paid exactly, a 7 by mistake, a child and closing time:
=== TICKET MACHINE - LINE 12 === Ticket (1 general, 2 young, 3 child, 0 close): 1 Due 1.50 €. Insert money: 1 Due 0.50 €. Insert money: 1 Ticket issued. Change: 0.50 € Ticket (1 general, 2 young, 3 child, 0 close): young Invalid option Ticket (1 general, 2 young, 3 child, 0 close): 2 Due 1.05 €. Insert money: 1.05 Ticket issued. Change: 0.00 € Ticket (1 general, 2 young, 3 child, 0 close): 7 Invalid option Ticket (1 general, 2 young, 3 child, 0 close): 3 Ticket issued. Change: 0.00 € Ticket (1 general, 2 young, 3 child, 0 close): 0 --- End of the day --- Riders served: 3 Collected: 2.55 €
Checking by hand: the general rider pays 1 + 1 = €2.00 for a €1.50 ticket, so the change is €0.50 and the payment loop does two laps (1.50 due and then 0.50). The young rider pays exactly 1.05: one lap and change 0.00. The child never enters the payment loop. Collected: 150 + 105 + 0 = 255 cents = €2.55. And young and the 7 do not count as riders: the do…while rejects them before they reach riders++.
The output of a program with scanf in your terminal will look just like this: what you type appears after each question because the terminal echoes it as you type.
Questions you might ask
Why while (1) and not while (type != 0)? Because the type is asked inside the loop. With while (type != 0) you would have to ask for the type before the loop and again at the end of the body (the same do…while twice). With while (1) and a break right after reading the type, the question is written only once.
Why cents and not float for the price? Because comparisons with decimals fail: 1.05 cannot be stored exactly in binary. In cents, 105 is 105. The general rule: money is counted in the smallest unit and with integers.
What exactly does while ((c = getchar()) != '\n' && c != EOF); do? It reads and throws away characters up to the Enter. When scanf("%d") cannot read young, those letters stay waiting; without this line, the next lap of the do…while would find them again and the loop would never end. The final ; is its body, empty on purpose.
Why type = -1 when scanf fails? Because scanf does not touch type if it reads nothing: it would keep the previous rider’s value (for example, a valid 1) and the do…while would accept input nobody has typed. With -1 we force the bad-input condition.
And what if the rider puts in €5 for a €1.50 ticket? The while does one lap, paid becomes 500 and the change is 500 - 150 = 350: €3.50.
Setting paid = 0 only once, before the while (1). The first rider pays fine, but the second one starts with 200 cents “already paid” and the machine gives them the ticket without asking for anything.
Try it yourself
- Add a 10-journey pass for €12.00 (option 4) and check that the payment loop does several laps with €2 coins.
- Count how many tickets of each type are sold and show it at closing time.
Program 2: number series calculator
The program asks for an n between 1 and 15 and calculates four series with for: the sum 1 + 2 + … + n, the factorial n!, the first n Fibonacci terms and the powers of 2 from 2⁰ to 2ⁿ. What makes it tricky is not the loop but everything around it: each accumulator starts at a different value, and the numbers grow so fast that an int falls short.
The idea
Every series follows the accumulator pattern:
- Give it a starting value before the loop: 0 to add, 1 to multiply.
- On every lap, combine it with
i:sum += i,factorial *= i,power *= 2. - Use it after the loop.
Fibonacci is special: each term depends on the two previous ones, so you need three variables that pass the baton along.
Before coding, write out the first three laps of each series by hand. If the table works on paper, the code is just copying it.
The code
#include <stdio.h>
int main(void) {
int n, i;
long long sum, factorial, power; // long long: 64 bits on Windows too
int previous, current, next; // the three for Fibonacci
int c;
printf("=== SERIES CALCULATOR ===\n");
// Ask for n with validation: at least once and until it is between 1 and 15
do {
printf("Enter n (1-15): ");
if (scanf("%d", &n) != 1) {
while ((c = getchar()) != '\n' && c != EOF);
n = 0;
}
if (n < 1 || n > 15) {
printf("Error: n must be between 1 and 15\n");
}
} while (n < 1 || n > 15);
// Series 1: sum 1 + 2 + ... + n (a sum accumulator starts at 0)
sum = 0;
for (i = 1; i <= n; i++) {
sum += i;
}
printf("\nSum 1 to %d: %lld\n", n, sum);
printf("Formula n*(n+1)/2: %d\n", n * (n + 1) / 2);
// Series 2: factorial n! (a product accumulator starts at 1, not 0)
factorial = 1;
for (i = 1; i <= n; i++) {
factorial *= i;
}
printf("\n%d! = %lld\n", n, factorial);
// Series 3: the first n Fibonacci terms
printf("\nFibonacci (%d terms): ", n);
previous = 0;
current = 1;
for (i = 1; i <= n; i++) {
printf("%d ", previous);
next = previous + current; // the new term
previous = current; // everything moves one place
current = next;
}
printf("\n");
// Series 4: powers of 2, from 2^0 to 2^n
printf("\nPowers of 2:\n");
power = 1;
for (i = 0; i <= n; i++) {
printf("2^%2d = %lld\n", i, power);
power *= 2;
}
return 0;
}
Zooming in on Fibonacci, the hardest part:
previous = 0;
current = 1;
for (i = 1; i <= n; i++) {
printf("%d ", previous);
next = previous + current; // the new term
previous = current; // everything moves one place
current = next;
}
The order of the three assignments matters. First next is calculated with the current values; then previous takes the value of current, and current takes that of next. If you change the order and write previous = current before working out the sum, you lose the old value of previous and the series comes out wrong.
i | Printed (previous) | next = previous + current | previous becomes | current becomes |
|---|---|---|---|---|
| 1 | 0 | 0 + 1 = 1 | 1 | 1 |
| 2 | 1 | 1 + 1 = 2 | 1 | 2 |
| 3 | 1 | 1 + 2 = 3 | 2 | 3 |
| 4 | 2 | 2 + 3 = 5 | 3 | 5 |
| 5 | 3 | 3 + 5 = 8 | 5 | 8 |
| 6 | 5 | 5 + 8 = 13 | 8 | 13 |
Output
With a 0 first (out of range) and then 6:
=== SERIES CALCULATOR === Enter n (1-15): 0 Error: n must be between 1 and 15 Enter n (1-15): 6 Sum 1 to 6: 21 Formula n*(n+1)/2: 21 6! = 720 Fibonacci (6 terms): 0 1 1 2 3 5 Powers of 2: 2^ 0 = 1 2^ 1 = 2 2^ 2 = 4 2^ 3 = 8 2^ 4 = 16 2^ 5 = 32 2^ 6 = 64
Checking by hand: 1 + 2 + 3 + 4 + 5 + 6 = 21, the same as the formula 6 · 7 / 2 = 21. The factorial, 1 · 2 · 3 · 4 · 5 · 6 = 720. Fibonacci, the six from the table: 0 1 1 2 3 5. And the last power, 2⁶ = 64. Notice that the powers loop goes from i = 0 to i <= n: that is n + 1 lines (seven with n = 6), because 2⁰ counts too.
Questions you might ask
Why long long and not int? Because an int has 32 bits and its maximum is 2 147 483 647. 13! is already 6 227 020 800. Look what happens with int:
#include <stdio.h>
int main(void) {
int i;
int factorial = 1; // int: 32 bits, at most 2 147 483 647
for (i = 1; i <= 15; i++) {
factorial *= i;
if (i >= 11) {
printf("%2d! = %d\n", i, factorial);
}
}
return 0;
}
11! = 39916800 12! = 479001600 13! = 1932053504 14! = 1278945280 15! = 2004310016
Up to 12! it is fine (479 001 600). From 13! on the numbers are garbage: 13! is not 1 932 053 504, and 14! cannot be smaller than 13!. It is an overflow, and in C, with a signed int, that is undefined behaviour: on your computer you might get other numbers. With long long (64 bits), 15! = 1 307 674 368 000 fits easily.
Why not long, like in the old version of this article? Because long is 64 bits on Fedora but 32 on Windows. long long is at least 64 everywhere.
Why does the factorial start at 1 and the sum at 0? Because they are the neutral elements: adding 0 changes nothing and neither does multiplying by 1. If the factorial started at 0, the whole product would be 0.
Why limit n to 15? Because of the powers table and the factorial: with long long, the factorial holds up to 20!, but Fibonacci’s int would overflow around term 47. The limit is there so the program never shows a false number.
Declaring the accumulator without a starting value (long long factorial;). In C, an uninitialised local variable holds whatever was in that memory, and the result is garbage: you will see it in the mistakes section.
Try it yourself
- Add the series of squares (1, 4, 9, …, n²) and its sum, and check that with n = 6 the sum is 91.
- Change the limit to 20 and check which series breaks first (hint: Fibonacci uses
int).
Program 3: guess the number with 7 attempts
This is the promise from conditionals: the guessing game, which had one attempt there, now has seven. The secret number is 42 (fixed, so the game can be repeated), and after each attempt the program says whether you went too high or too low and how close you are. What makes it tricky: there are two ways to finish (guessing it or running out of attempts) and an attempt typed in letters should not count.
The idea
A for from 1 to 7 counts the attempts, and a flag guessed lets you leave early:
for (attempt = 1; attempt <= MAX_ATTEMPTS && !guessed; attempt++)
The condition has two parts joined with &&: “there are attempts left” and “you have not guessed it yet”. As soon as one of them is false, the loop ends. After the loop, the flag tells you which one it was.
Before coding, think about what attempt is worth on exit in each case. If you guess it on attempt 5, that lap’s attempt++ still runs before the condition is checked, so attempt is 6.
The code
#include <stdio.h>
#define MAX_ATTEMPTS 7
int main(void) {
int secret = 42; // fixed so the game can be repeated
int number; // what the player types
int attempt; // 1, 2, 3... up to MAX_ATTEMPTS
int guessed = 0; // flag: 0 = not yet, 1 = guessed
int difference;
int c;
printf("=== GUESS THE NUMBER ===\n");
printf("I have a number between 1 and 100. You have %d tries.\n\n", MAX_ATTEMPTS);
// Two ways out: run out of attempts or guess it (the flag)
for (attempt = 1; attempt <= MAX_ATTEMPTS && !guessed; attempt++) {
printf("Attempt %d of %d: ", attempt, MAX_ATTEMPTS);
if (scanf("%d", &number) != 1 || number < 1 || number > 100) {
while ((c = getchar()) != '\n' && c != EOF);
printf("It must be a number between 1 and 100 (doesn't count)\n");
attempt--; // the for's attempt++ leaves it as it was
continue;
}
// Distance to the secret, always positive
difference = number > secret ? number - secret : secret - number;
if (difference == 0) {
guessed = 1; // the for will see it at its next check
printf("Correct!\n");
} else {
printf(number > secret ? "Too high" : "Too low");
if (difference <= 3) {
printf(" - very close!\n");
} else if (difference <= 10) {
printf(" - close\n");
} else if (difference <= 25) {
printf(" - far\n");
} else {
printf(" - very far\n");
}
}
}
// On exit, attempt is ONE MORE than the last attempt used
if (guessed) {
printf("\nYou got it in %d attempt(s)\n", attempt - 1);
} else {
printf("\nOut of attempts. The number was %d\n", secret);
}
return 0;
}
Zooming in on how bad input is handled, which is the new bit:
printf("Attempt %d of %d: ", attempt, MAX_ATTEMPTS);
if (scanf("%d", &number) != 1 || number < 1 || number > 100) {
while ((c = getchar()) != '\n' && c != EOF);
printf("It must be a number between 1 and 100 (doesn't count)\n");
attempt--; // the for's attempt++ leaves it as it was
continue;
}
If scanf fails or the number is out of range, the line is emptied, a warning is shown and attempt-- is done before the continue. Why? Because continue in a for jumps to the update attempt++: the -- and the ++ cancel out and the player repeats the same attempt. It is the difference between continue in a for and in a while that we saw in the theory, used to our advantage.
Output
A game where the number is guessed (with a forty typed in letters in the middle):
=== GUESS THE NUMBER === I have a number between 1 and 100. You have 7 tries. Attempt 1 of 7: 50 Too high - close Attempt 2 of 7: 25 Too low - far Attempt 3 of 7: 37 Too low - close Attempt 4 of 7: forty It must be a number between 1 and 100 (doesn't count) Attempt 4 of 7: 44 Too high - very close! Attempt 5 of 7: 42 Correct! You got it in 5 attempt(s)
And another where all seven attempts are used up:
=== GUESS THE NUMBER === I have a number between 1 and 100. You have 7 tries. Attempt 1 of 7: 50 Too high - close Attempt 2 of 7: 25 Too low - far Attempt 3 of 7: 37 Too low - close Attempt 4 of 7: 45 Too high - very close! Attempt 5 of 7: 40 Too low - very close! Attempt 6 of 7: 43 Too high - very close! Attempt 7 of 7: 41 Too low - very close! Out of attempts. The number was 42
Checking the hints in the first game: 50 − 42 = 8, which is 10 or less: “close”. 42 − 25 = 17, between 11 and 25: “far”. 42 − 37 = 5: “close”. 44 − 42 = 2, 3 or less: “very close”. And forty does not use up an attempt: the program says “Attempt 4 of 7” again. When it is guessed on the fifth, attempt is 6 on exit and the message uses attempt - 1 = 5.
Questions you might ask
Why a flag and not a break when it is guessed? Both work. With break, attempt would be 5 on exit and not 6 (the update does not run), and you would have to change the attempt - 1 at the end. With the flag, the for condition tells the whole story: “while there are attempts left and you have not guessed it”.
Why !guessed and not guessed == 0? They are the same: in C, 0 is false and any other value is true, so !guessed is true when guessed is 0. It reads as “not guessed”.
What happens if I remove attempt--? The attempt in letters would count as used: “Attempt 4 of 7: forty” and the next question would already be “Attempt 5 of 7”.
Why printf(number > secret ? "Too high" : "Too low")? The ternary picks which of the two strings gets printed; it saves an if…else for something where only one word changes. We saw it in conditionals.
Writing the condition with || instead of &&: attempt <= 7 || !guessed. With ||, after guessing it would keep asking up to the seventh attempt, and if you never guess it, the second part (!guessed) would be true forever: infinite loop.
Try it yourself
- Make the program say at the end how many attempts the player had left.
- Replace the secret with one that depends on the time (
time(NULL) % 100 + 1, with#include <time.h>) and check that seven attempts are always enough if you keep halving: 100 → 50 → 25 → 13 → 7 → 4 → 2 → 1.
The pattern to take away
The three programs repeat three patterns you will see in every IC2 practical exam:
// PATTERN 1: do...while to validate a value
do {
printf("Value: ");
if (scanf("%d", &value) != 1) {
while ((c = getchar()) != '\n' && c != EOF); // throw the letters away
value = BAD_VALUE;
}
} while (/* value not valid */);
// PATTERN 2: for with an accumulator
accumulated = NEUTRAL; // 0 to add, 1 to multiply
for (i = 1; i <= n; i++) {
accumulated = accumulated OPERATION i;
}
// PATTERN 3: loop with two ways out (flag)
int done = 0;
for (attempt = 1; attempt <= MAX && !done; attempt++) {
/* ... */
if (/* success condition */) {
done = 1;
}
}
Common mistakes in this practice
Mistake 1 — Inverted condition in a do…while
The do…while condition is the one to repeat, that is, the bad-input one. If you write the good-input one (while (type >= 0 && type <= 3)), the loop repeats exactly when the input is right and accepts wrong input the first time.
Mistake 2 — Not initialising the accumulator
#include <stdio.h>
int main(void) {
long long factorial; // no initial value
int i;
for (i = 1; i <= 5; i++) {
factorial *= i;
}
printf("5! = %lld\n", factorial);
return 0;
}
5! = 16888202095383360
5! is 120, not that number. factorial started with whatever garbage was in that memory location, and the multiplication carried it along. gcc did not warn with -Wall in this build, and on your computer you might get another number (or even 0). Always: long long factorial = 1;.
Mistake 3 — Semicolon after the while
while (paid < price); is a loop with an empty body. If money is due when it gets there, the condition is true and nothing inside the loop changes it: the program hangs without printing anything. If nothing is due (child ticket), the block of braces below runs once and everything seems fine. It is one of the hardest mistakes to spot because there is no output to warn you.
And three more that show up a lot in these programs:
- Resetting in the wrong place:
paid = 0outside the riders loop, orsum = 0inside the sumfor. - Forgetting to empty the input after a failed
scanf: one letter and thedo…whilespins forever. intfor numbers that grow: factorials, powers and products overflow without warning beyond about 2,100 million.
Watch it step by step in Python Tutor
Python Tutor (pythontutor.com) runs the code line by line and shows the value of each variable. Choose C and paste the Fibonacci loop, without scanf and with 6 terms:
#include <stdio.h>
int main(void) {
int i;
int previous = 0, current = 1, next;
for (i = 1; i <= 6; i++) {
printf("%d ", previous);
next = previous + current;
previous = current;
current = next;
}
printf("\n");
return 0;
}
Output:
0 1 1 2 3 5
What to watch step by step:
- On the first lap,
nexthas no value until its line runs: Python Tutor shows it empty or with garbage. - Notice the order: first
nextchanges, thenpreviousand finallycurrent. At no point is a value lost that is needed later. - On lap 3,
previousandcurrentare 1 and 2: the window from the Fibonacci figure. - On exit,
iis 7 andcurrentis 13, one term more than was printed.
Summary
- Every question that repeats is a loop: “another rider?”, “valid input?”, “money still due?”.
do…whileto validate (at least once),whilewhen there may be no laps at all (the child does not pay) andforwhen you know how many.- Accumulators are set up before the loop with their neutral value: 0 to add, 1 to multiply.
- Money in cents and with integers; numbers that grow, in
long long. - A loop with two ways out is written with a flag in the condition, and on exit the control variable is one more than the last lap.
- Every
scanfinside a loop gets checked and, if it fails, the input gets emptied.
Checklist: have you practised it?
- Have you run the three programs and checked the outputs?
- Have you tried the “Try it yourself” tasks without looking at the theory?
- Can you explain why the machine charges one cent too much without the
+ 0.5f? - Could you write from memory the
do…whilethat validates a number and throws the letters away? - Do you know what
attemptis worth on exit if you guess it on the third attempt? - Do you know why 13! comes out wrong with
int?
If you answered yes to almost everything, you are ready for the loops in C exercises, where I no longer give you the code. See you there!
Next step: exercisesLoops in C — exercises with primes, rock-paper-scissors, a 3-attempt login and a menu calculator (and the ; that left the list of primes empty)Did this article help you? Here is how you can support SergioLearns →




What is an operating system — theory with your laptop on a study afternoon, a write() call and a CPU that never rests (and the three programs that weren’t running at the same time)