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.

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 int 1 or 0.
  • How an if / else if / else chain works: only the first true branch runs.
  • The &&, || and ! operators, and that they use short-circuit evaluation.
  • That in a switch every case needs its break.

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:

Grade scale of the classifier Bar from 0 to 10 with four bands: Fail from 0 to 5, Pass from 5 to 7, Merit from 7 to 9 and Distinction from 9 to 10. Marks at 7.5 (Merit) and 4.25 (Fail). Below, the order in which the program asks. The four bands of the classifier Each else if only needs the lower limit: the upper one was ruled out by the branch before Fail Pass Merit Distinct. 0 1 2 3 4 5 6 7 8 9 10 7.5 → Merit 4.25 → Fail Order of the questions 1. grade >= 9.0f 2. grade >= 7.0f 3. grade >= 5.0f 4. else
With 7.5 the first question fails and the second one succeeds: Merit. With 4.25 all three fail and it reaches the else.

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.

Typedscanf returnsgrade < 0 || grade > 10What happens
7.510 || 0 → 0Merit, pass, 2.50 to spare
4.2510 || 0 → 0Fail, 0.75 missing
seven0(never reached)“that is not a number” and return 1
1110 || 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 / else and 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:

Proximity bands around 42 Number line from 1 to 100 with the secret at 42 and three bands: very close from 37 to 47, close from 27 to 57 and far from 12 to 72. Outside, very far. 55 lands in close (difference 13) and 10 in very far (difference 32). The hint bands with secret = 42 difference <= 5, <= 15, <= 30: from the narrowest to the widest far (<= 30) close (<= 15) very close 1 12 27 37 42 47 57 72 100 secret 55: close (13) 10: very far (32) very far
The bands sit inside each other: that is why the narrowest one is asked first. 55 is 13 away from 42: "Close".

Let’s walk the hint chain with the four attempts I ran:

AttemptDifference== 0<= 5<= 15<= 30Message
4201———Score: perfect
5513001—Close
10320000Very 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:

Flow of the calculator The option is read and checked to be between 1 and 5 (otherwise error and return 1), A and B are read and the switch jumps to the case. With 4, 7 and 3 it goes to case 4, checks b == 0 and divides: 7.00 / 3.00 = 2.3333. In case 5 it looks at (int)b: with 0.5 it gives 0 and an error. The calculator with 4, 7 and 3 Validate first, then ask; inside case 4 there is a nested if scanf(option) → 4 1 <= option <= 5 no Error + return 1 yes read A = 7, B = 3 switch (option) case 1-3 case 4 case 5 default if (b == 0) else 7.00 / 3.00 = 2.3333 if ((int)b == 0) with B = 0.5: (int)0.5 is 0 → Error: modulo by zero
The coral path is the run with 4, 7 and 3. Case 5 checks (int)b because that is the number % really divides by.

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;
ExpressionWith b = 3With b = 0.5With b = 0
b == 0001
(int)b300
(int)b == 0011
What does case 5 do?CalculatesError (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 a char with scanf(" %c", &op). Does switch still work?

Common mistakes in this practice

  • The ; after the if. It turns the if into 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 1 always runs and the program never classifies.
  • Not checking what scanf returns. 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 next case runs too, without checking anything.
  • Doing a switch on a float. 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:

  1. In the variables panel, b appears with the value 0 from the declaration line.
  2. When switch (option) runs, the arrow jumps straight to case 4: case 1 is not touched.
  3. Inside case 4, if (b == 0) holds and the error printf runs; the division line never runs.
  4. The break takes the arrow to return 0 without going through default.
  5. Change b to 2.0f and repeat: now the arrow goes into the else and 3.5000 is printed.

Summary

  • Validate the data first (what scanf returns and the range) and leave with return 1 if anything fails: the rest of the program can trust it.
  • In an else if chain 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 < 1 does not look at attempt if the read failed.
  • switch for exact integer values, with a break in every case and a default as a safety net; a case can contain an if.
  • (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 scanf returns 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 == 0 and not b == 0?
  • Have you compiled with -Wall -Wextra and 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 →

Sigue aprendiendo

Leave a Reply

Your email address will not be published. Required fields are marked *