Conditionals in C — practice with a grade classifier, a one-shot guessing game and a switch calculator (and the semicolon that failed me with a 7.5)
My IC2 midterm grade came out while I was on the bus up to Tafira. A 7.5. Happy, I opened on my laptop the grade classifier I had written the week before to check “officially” that I had passed. And the program came out with this:
#include <stdio.h>
int main(void) {
float grade = 7.5f; // my IC2 grade
if (grade < 5.0f); // the ; swallows the if
{
printf("You need %.2f more points to pass\n", 5.0f - grade);
}
return 0;
}
Output:
You need -2.50 more points to pass
I needed minus two point five points to pass. It did not even make sense as a sentence. The problem was a ; right after the bracket of the if: that semicolon is the whole body of the if, and the block below stands on its own and always runs, whatever the grade (5 − 7.5 = −2.5). The funny part is that gcc with -Wall was telling me:
hook.c: In function 'main': hook.c:6: warning: this 'if' clause does not guard... [-Wmisleading-indentation] hook.c:7: note: ...this statement, but the latter is misleadingly indented as if it were guarded by the 'if'
The lesson I took away, and the one you will see again in today’s three programs: the shape of the code does not guarantee what it does; what counts are the brackets, the braces, the break statements and the compiler warnings.
In the C conditionals theory we saw the if / else if / else chain, logical operators, the ternary and switch. Now it is time to write whole programs with data that is not always the nice kind: a grade classifier that does not trust what is typed, a number guessing game with a single attempt (loops come in the next topic) and a calculator with a menu built with switch. All three are medium level and all three read with scanf: every run you will see is real, with what was typed after each prompt. Open gedit in Fedora and type them as you read.
Table of Contents
Before you start
It helps to be clear about:
- That in C a condition is a number: 0 is false and anything else is true, and a comparison returns the
int1 or 0. - How an
if/else if/elsechain works: only the first true branch runs. - The
&&,||and!operators, and that they use short-circuit evaluation. - That in a
switcheverycaseneeds itsbreak.
If any of these sounds shaky, go over the theory before carrying on.
Compile each program with gcc -Wall -Wextra program.c -o program and run it with ./program. If your setup is not ready, follow the guide to install gedit in Fedora and compile with gcc first.
Program 1: grade classifier with if / else if
A program that asks for a grade from 0 to 10 and tells you its mark (Fail, Pass, Merit or Distinction; in Spain Suspenso, Aprobado, Notable and Sobresaliente), whether it is a pass, how many points you have to spare or are missing, and whether you can apply for Honours (Matrícula de Honor). What makes it tricky: the user may type an impossible grade (11) or something that is not a number at all (“seven”), and the program has to notice before classifying anything.
The idea
Three steps, in this order: read and check it is a number, check it is between 0 and 10 and classify. If either check fails, the program ends right there with return 1. To classify, an else if chain that asks from the highest grade to the lowest; that way each branch only needs the lower limit.
Before coding, think about what scanf returns: not the grade, but how many items it managed to read. If you ask it for a %f and type “seven”, it returns 0.
The code
#include <stdio.h>
int main(void) {
float grade;
printf("=== GRADE CLASSIFIER ===\n\n");
printf("Enter your grade (0-10): ");
// scanf returns how many items it read: if it is not 1, we did not get a number
if (scanf("%f", &grade) != 1) {
printf("Error: that is not a number\n");
return 1;
}
// We validate the range BEFORE classifying: an 11 has no possible grade
if (grade < 0 || grade > 10) {
printf("Error: the grade must be between 0 and 10\n");
return 1;
}
// From the highest grade to the lowest: each else if knows the ones above failed
if (grade >= 9.0f) {
printf("Grade: Distinction\n");
} else if (grade >= 7.0f) {
printf("Grade: Merit\n");
} else if (grade >= 5.0f) {
printf("Grade: Pass\n");
} else {
printf("Grade: Fail\n");
}
// C has no True/False: a comparison is 1 or 0 and can be stored in an int
int passed = (grade >= 5.0f);
printf("Passed? (1 = yes, 0 = no): %d\n", passed);
// if (passed) is the same as if (passed != 0)
if (passed) {
printf("You are %.2f points above the pass mark\n", grade - 5.0f);
} else {
printf("You need %.2f more points to pass\n", 5.0f - grade);
}
// The ternary picks one of two texts in a single line
printf("Honours: %s\n", (grade >= 9.0f) ? "you can apply" : "no");
return 0;
}
Output
With a 7.5:
=== GRADE CLASSIFIER === Enter your grade (0-10): 7.5 Grade: Merit Passed? (1 = yes, 0 = no): 1 You are 2.50 points above the pass mark Honours: no
Check it by hand: 7.5 is not ≥ 9, but it is ≥ 7, so “Merit” and the chain ends. 7.5 >= 5.0 is 1, which is what gets stored in passed and what the %d prints. Since 1 is true, it takes the first path of the second if: 7.5 − 5 = 2.50. And 7.5 >= 9.0 is false, so the ternary picks “no”.
The drawing shows the four bands and where each grade lands:
With a 4.25 (a separate run, same program):
=== GRADE CLASSIFIER === Enter your grade (0-10): 4.25 Grade: Fail Passed? (1 = yes, 0 = no): 0 You need 0.75 more points to pass Honours: no
All three questions in the chain fail and it reaches the else: Fail. passed is 0, so if (passed) goes to the else and works out 5 − 4.25 = 0.75. And the two bad inputs. First, typing “seven”:
=== GRADE CLASSIFIER === Enter your grade (0-10): seven Error: that is not a number
Then an 11:
=== GRADE CLASSIFIER === Enter your grade (0-10): 11 Error: the grade must be between 0 and 10
With “seven”, scanf cannot convert anything into a float and returns 0; 0 != 1 is true and the program complains. With 11, scanf does read the number, but 11 > 10 makes the || true.
| Typed | scanf returns | grade < 0 || grade > 10 | What happens |
|---|---|---|---|
7.5 | 1 | 0 || 0 → 0 | Merit, pass, 2.50 to spare |
4.25 | 1 | 0 || 0 → 0 | Fail, 0.75 missing |
seven | 0 | (never reached) | “that is not a number” and return 1 |
11 | 1 | 0 || 1 → 1 | “must be between 0 and 10” and return 1 |
Questions you might ask
Why does else if (grade >= 7.0f) not also check that grade < 9.0f? Because if we reached that line, the previous question (grade >= 9.0f) failed. The chain already guarantees the grade is below 9. Writing it again is not wrong, but it is noise.
Why 5.0f with the f? Because grade is a float. Plain 5.0 is a double; C would convert grade into a double to compare and it would work the same, but with the f both sides have the same type and there are no hidden conversions. You saw this in Types and variables in C.
What is int passed = (grade >= 5.0f); about? Storing the result of a comparison. Since in C a comparison is an int worth 1 or 0, you can keep it in a variable and use it later as a condition: if (passed) reads “if passed is not 0”.
What is return 1 for? It ends main there and then and tells the system something went wrong (0 means “all good”). If you type echo $? in the terminal right after running the program, you will see that 1.
Why the ternary and not another if? Because it only picks between two texts for the same printf. With an if / else you would have two almost identical printf calls.
Classifying from the bottom up with >=: if (grade >= 5) Pass; else if (grade >= 7) Merit…. A 9.5 is ≥ 5, takes the first branch and comes out as “Pass”. With >=, go from the highest grade to the lowest.
Try it yourself
- Add Honours as a fifth mark for an exact 10, without breaking the order of the chain (where must that branch go?).
- Replace the ternary on the last line with an
if/elseand compare how many lines each version takes.
Program 2: guess the number with a single attempt
The program “thinks of” a number (42), lets you try once and tells you whether you went too high or too low, and how close you were. No loops yet: in the Loops in C topic we will turn it into a real game with several attempts. What makes it tricky: working out the distance without it going negative and getting the order of the hint bands right.
The idea
First, validate with a single if: scanf must have read a number and it must be between 1 and 100. Then, the distance: if the attempt is larger than the secret, attempt − secret; otherwise, secret − attempt. That way it is always positive without needing abs() (which exists, but lives in <stdlib.h>). Finally, two independent decisions: “high, low or spot on” and “how close”.
Before coding, think about the hint bands: “5 or less away”, “15 or less”, “30 or less”. They sit inside each other, so which one do you have to ask first?
The code
#include <stdio.h>
int main(void) {
int secret = 42; // fixed so the game can be repeated
int attempt;
int difference;
printf("=== GUESS THE NUMBER ===\n");
printf("I have a number between 1 and 100. You get ONE attempt.\n\n");
printf("Your attempt: ");
// If scanf fails, the || is already true and C does not look at attempt (it would have no value)
if (scanf("%d", &attempt) != 1 || attempt < 1 || attempt > 100) {
printf("It must be a number between 1 and 100\n");
return 1;
}
// Distance without abs(): always the larger minus the smaller, so it is never negative
if (attempt > secret) {
difference = attempt - secret;
} else {
difference = secret - attempt;
}
// High, low or spot on?
if (attempt == secret) {
printf("Correct! The number was %d\n", secret);
} else if (attempt > secret) {
printf("Too high\n");
} else {
printf("Too low\n");
}
// Proximity hint: from the narrowest band to the widest
if (difference == 0) {
printf("Score: perfect\n");
} else if (difference <= 5) {
printf("Very close: difference of %d\n", difference);
} else if (difference <= 15) {
printf("Close: difference of %d\n", difference);
} else if (difference <= 30) {
printf("Far: difference of %d\n", difference);
} else {
printf("Very far: difference of %d\n", difference);
}
return 0;
}
Output
With a 55:
=== GUESS THE NUMBER === I have a number between 1 and 100. You get ONE attempt. Your attempt: 55 Too high Close: difference of 13
By hand: 55 > 42, so difference = 55 − 42 = 13 and “Too high”. For the hint, 13 == 0 no; 13 <= 5 no; 13 <= 15 yes: “Close”. This is what the bands look like:
Let’s walk the hint chain with the four attempts I ran:
| Attempt | Difference | == 0 | <= 5 | <= 15 | <= 30 | Message |
|---|---|---|---|---|---|---|
| 42 | 0 | 1 | — | — | — | Score: perfect |
| 55 | 13 | 0 | 0 | 1 | — | Close |
| 10 | 32 | 0 | 0 | 0 | 0 | Very far (else) |
| 150 | — | — | — | — | — | Error: never worked out |
The exact hit:
=== GUESS THE NUMBER === I have a number between 1 and 100. You get ONE attempt. Your attempt: 42 Correct! The number was 42 Score: perfect
A 10:
=== GUESS THE NUMBER === I have a number between 1 and 100. You get ONE attempt. Your attempt: 10 Too low Very far: difference of 32
And a number out of range:
=== GUESS THE NUMBER === I have a number between 1 and 100. You get ONE attempt. Your attempt: 150 It must be a number between 1 and 100
With the 10: 10 is not larger than 42, so difference = 42 − 10 = 32. No band covers it (32 > 30) and it reaches the else. With 150, the validation || becomes true at attempt > 100 and the program ends without working anything out.
Questions you might ask
Why is scanf inside the if? Because that way it reads and checks in the same line. And thanks to short-circuit evaluation: if scanf(...) != 1 is true (nothing was read), the || already knows the result is true and does not evaluate attempt < 1. That matters, because in that case attempt would have no value stored.
Why not just difference = attempt - secret;? Because with the 10 it would give −32, and -32 <= 5 is true: the program would say “Very close”. The if that always subtracts the smaller from the larger avoids that disaster.
Why do the bands go from the narrowest to the widest? Because anything 5 or less away is also 15 or less away. If you asked difference <= 30 first, 55 (13 away) and 41 (1 away) would both say “Far”.
Do we need two separate chains? Yes: they are two different questions. The first gives the direction (high, low) and the second, the distance. If you merged them into one chain, you would have to repeat each band for “high” and for “low”.
Comparing with = in if (attempt = secret). It stores 42 in attempt, is worth 42 (true) and the program congratulates everyone. -Wall warns about it.
Try it yourself
- Add an “Almost there” band for a difference of 1. Between which two branches does it go?
- Replace the distance calculation with
abs(attempt - secret), including<stdlib.h>, and check that 10 still gives 32.
Program 3: calculator with a switch menu
A calculator that shows a menu of five operations, asks for the option and the two numbers, and shows the result. What makes it tricky: dividing by zero, the modulo (which only exists for integers) and an option that is not on the menu. It is exactly the kind of program that turns up in the IC2 labs.
The idea
Validate the option before asking for the numbers (if it is a 9, there is no point asking for anything else). Then a switch on the option, because it is an integer compared with exact values. Inside the dangerous cases (4 and 5), a nested if that checks the divisor before dividing.
Before coding, think about which number the % operator really divides by when you pass it (int)b. If b is 0.5, is it 0 or not?
The code
#include <stdio.h>
int main(void) {
int option;
float a, b;
printf("=== CALCULATOR ===\n");
printf("1. Addition\n");
printf("2. Subtraction\n");
printf("3. Multiplication\n");
printf("4. Division\n");
printf("5. Modulo (integers)\n");
printf("\nChoose operation (1-5): ");
scanf("%d", &option);
// We validate BEFORE asking for the numbers: if the option does not exist, there is no point going on
if (option < 1 || option > 5) {
printf("Error: invalid option\n");
return 1;
}
printf("Number A: ");
scanf("%f", &a);
printf("Number B: ");
scanf("%f", &b);
printf("\n--- Result ---\n");
switch (option) {
case 1:
printf("%.2f + %.2f = %.2f\n", a, b, a + b);
break;
case 2:
printf("%.2f - %.2f = %.2f\n", a, b, a - b);
break;
case 3:
printf("%.2f * %.2f = %.2f\n", a, b, a * b);
break;
case 4:
if (b == 0) { // an if inside a case: you can nest without limit
printf("Error: cannot divide by zero\n");
} else {
printf("%.2f / %.2f = %.4f\n", a, b, a / b);
}
break;
case 5:
// % only works with integers: (int) truncates, so 0.5 becomes 0.
// That is why we check (int)b and not b: it is the number we really divide by
if ((int)b == 0) {
printf("Error: modulo by zero does not exist\n");
} else {
printf("%d %% %d = %d\n", (int)a, (int)b, (int)a % (int)b); // %% prints a %
}
break;
default: // we validated above, but it saves you if you extend the menu
printf("Invalid option\n");
break;
}
return 0;
}
Output
Option 4 with 7 and 3:
=== CALCULATOR === 1. Addition 2. Subtraction 3. Multiplication 4. Division 5. Modulo (integers) Choose operation (1-5): 4 Number A: 7 Number B: 3 --- Result --- 7.00 / 3.00 = 2.3333
7 divided by 3 is 2.3333… and %.4f keeps four decimals. The full route, with the path this run took:
More runs of the same program:
=== CALCULATOR === 1. Addition 2. Subtraction 3. Multiplication 4. Division 5. Modulo (integers) Choose operation (1-5): 5 Number A: 17 Number B: 5 --- Result --- 17 % 5 = 2
17 % 5: 5 fits 3 times into 17 (15) and 2 are left over. In the printf, %% prints a literal %.
Dividing by 0:
=== CALCULATOR === 1. Addition 2. Subtraction 3. Multiplication 4. Division 5. Modulo (integers) Choose operation (1-5): 4 Number A: 7 Number B: 0 --- Result --- Error: cannot divide by zero
The modulo by 0.5:
=== CALCULATOR === 1. Addition 2. Subtraction 3. Multiplication 4. Division 5. Modulo (integers) Choose operation (1-5): 5 Number A: 7 Number B: 0.5 --- Result --- Error: modulo by zero does not exist
And an option that does not exist:
=== CALCULATOR === 1. Addition 2. Subtraction 3. Multiplication 4. Division 5. Modulo (integers) Choose operation (1-5): 9 Error: invalid option
The modulo by 0.5 is the interesting one. b is 0.5, which is not 0, so an if (b == 0) check would let the modulo through. But (int)0.5 truncates to 0, and 7 % 0 is an integer division by zero: the program would die with Floating point exception. That is why case 5 checks (int)b. Zooming in on those lines:
case 5:
// % only works with integers: (int) truncates, so 0.5 becomes 0.
// That is why we check (int)b and not b: it is the number we really divide by
if ((int)b == 0) {
printf("Error: modulo by zero does not exist\n");
} else {
printf("%d %% %d = %d\n", (int)a, (int)b, (int)a % (int)b); // %% prints a %
}
break;
| Expression | With b = 3 | With b = 0.5 | With b = 0 |
|---|---|---|---|
b == 0 | 0 | 0 | 1 |
(int)b | 3 | 0 | 0 |
(int)b == 0 | 0 | 1 | 1 |
| What does case 5 do? | Calculates | Error (and survives) | Error |
Questions you might ask
Why validate the option outside the switch if there is already a default? So we do not ask for the two numbers for nothing. With 9 the program complains as soon as it reads it. The default stays as a safety net in case one day you add an option to the menu and forget to update the validation.
Can you put an if inside a case? Yes, and a switch inside another too. Inside a case you can put any statement; the only thing that closes it is the break.
Why is b == 0 reliable here if you should not compare float values with ==? Because 0 is stored exactly in a float: if the user types 0, b is exactly 0. The problem with == shows up with numbers like 1.10 or 0.1, which have no exact representation (we will see it in the exercises).
Why does each case end in break? Without the one in case 1, an addition would also print the subtraction, the multiplication… down to the next break. It is the fall-through you saw in the theory.
Checking if (b == 0) for the modulo. With b = 0.5 the check passes, (int)b is 0 and the program dies with Floating point exception (core dumped).
Try it yourself
- Add option 6, A squared (A × A), without breaking the validation or the
default. - Change the menu so the operation is chosen with letters (
's'for addition,'r'for subtraction…) reading acharwithscanf(" %c", &op). Doesswitchstill work?
Common mistakes in this practice
- The
;after theif. It turns theifinto an empty statement and the block below always runs (the “you need −2.50 more points” from the start). - Forgetting the braces. With
if (grade < 0) printf("Error\n"); return 1;without braces,return 1always runs and the program never classifies. - Not checking what
scanfreturns. If the user types letters, the variable is left without a value and the program works with garbage. - Getting the chain order wrong. From the highest grade to the lowest with
>=; from the narrowest band to the widest with<=. - Forgetting a
break. The nextcaseruns too, without checking anything. - Doing a
switchon afloat. It does not compile:switch quantity not an integer.
Watch it step by step in Python Tutor
Python Tutor runs the code line by line and shows the value of each variable at every step. Choose C as the language and paste this cut-down version of the calculator, with fixed data and the divisor set to 0:
#include <stdio.h>
int main(void) {
int option = 4;
float a = 7.0f, b = 0.0f;
switch (option) {
case 1:
printf("%.2f\n", a + b);
break;
case 4:
if (b == 0) {
printf("Error: division by zero\n");
} else {
printf("%.4f\n", a / b);
}
break;
default:
printf("Invalid option\n");
break;
}
return 0;
}
Output:
Error: division by zero
What to watch step by step:
- In the variables panel,
bappears with the value 0 from the declaration line. - When
switch (option)runs, the arrow jumps straight tocase 4:case 1is not touched. - Inside
case 4,if (b == 0)holds and the errorprintfruns; the division line never runs. - The
breaktakes the arrow toreturn 0without going throughdefault. - Change
bto2.0fand repeat: now the arrow goes into theelseand 3.5000 is printed.
Summary
- Validate the data first (what
scanfreturns and the range) and leave withreturn 1if anything fails: the rest of the program can trust it. - In an
else ifchain each branch knows the ones above failed: order them from the strictest to the broadest. - A comparison is an
int: it can be stored (int passed = grade >= 5;) and used as a condition. - Short-circuit evaluation in
&&and||protects you:scanf(...) != 1 || attempt < 1does not look atattemptif the read failed. switchfor exact integer values, with abreakin everycaseand adefaultas a safety net; acasecan contain anif.(int)truncates: check the value you are really going to use in the operation.
Checklist: have you practised it?
- Have you run the three programs and checked the outputs?
- Do you know what
scanfreturns when you type letters instead of a number? - Could you explain why the guessing game bands go from the narrowest to the widest?
- Do you know why the modulo checks
(int)b == 0and notb == 0? - Have you compiled with
-Wall -Wextraand read every warning? - Have you tried the “Try it yourself” ideas without looking at the theory?
If you said yes to nearly everything, you are ready for the C conditionals exercises, where I no longer give you the code. See you there!
Next step: exercisesConditionals in C — exercises with grades, triangles, a login without loops and bus fares (and the 1.10 € balance that was not 1.10 €)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)