The Statement That Was Promised

Every program in this course has run straight down the page: declare, compute, print, stop. Two lessons have already stopped at the same wall. Chapter 1 finished by printing scanf's conversion count and then computing with the input anyway, and this chapter's first exercise had to initialize a divisor to 1 rather than 0, because dividing by zero is undefined behaviour and nothing you had been given could inspect a divisor before using it. Both debts are paid here.

A branch needs a question, and C's questions are the six comparison operators: == equal, != not equal, and <, >, <=, >=. Each takes two values and produces an int, and only ever one of two ints: 1 when the relation holds and 0 when it does not. You have used one already and stored its result, in the previous lesson's int readable = (perms & can_read) != 0;. Note the doubled character in ==, because a single = is assignment and the two are different operators entirely. if puts such a value in parentheses and runs a block only when it is not zero. Two habits come with it and neither is negotiable in this course: always brace the body, even a single statement, because an unbraced body silently stops guarding the moment somebody adds a second line; and although any nonzero value counts as true, so if (converted) compiles and means if (converted != 0), write the comparison out, because if (converted != 2) states the question while the short form asks the reader to reconstruct it. Here is the program chapter 1 could not write, guarding both the conversion count and the divisor.

#include <stdio.h>

int main(void)
{
    int dividend = 0;
    int divisor = 0;

    int converted = scanf("%d %d", &dividend, &divisor);

    if (converted != 2)
    {
        printf("error: expected two numbers\n");
        return 1;
    }

    if (divisor == 0)
    {
        printf("error: cannot divide by zero\n");
        return 1;
    }

    printf("%d / %d is %d remainder %d\n", dividend, divisor, dividend / divisor, dividend % divisor);

    return 0;
}

Given the input 84 5 it prints 84 / 5 is 16 remainder 4; given hello it prints error: expected two numbers; given 84 0, error: cannot divide by zero. In neither of those last two runs does the division execute at all, which is the entire point. The 84 0 division would have been the undefined behaviour lesson 2 warned about, and no amount of checking afterwards can undo undefined behaviour. You check before, or not at all.

return 1; is doing two jobs at once. A return in main ends the program then and there, so it is also the way out of a branch, and chapter 1's first lesson established what the number means: 0 says the program succeeded and any nonzero value says it did not. Those three runs exit with 0, 1 and 1, so a script running this program can tell the good run from the bad ones without reading a word of its output. <stdlib.h> spells the same idea EXIT_FAILURE; a plain 1 is equally correct and is what this lesson uses.

Chains, and Conditions Built From Conditions

else runs when the condition was zero, and else if chains a second question onto the first, so exactly one branch of the chain runs and a final else catches everything the questions missed. Conditions themselves combine with three logical operators: && is and, || is or, and unary ! inverts. Like the comparisons, each yields an int that is 1 or 0.

#include <stdio.h>

int main(void)
{
    int dividend = 0;
    int divisor = 0;

    int converted = scanf("%d %d", &dividend, &divisor);

    if (converted != 2)
    {
        printf("error: expected two numbers\n");
    }
    else if (divisor != 0 && dividend % divisor == 0)
    {
        printf("%d divides by %d exactly %d times\n", dividend, divisor, dividend / divisor);
    }
    else
    {
        printf("no exact division to report\n");
    }

    return 0;
}

84 7 reports 84 divides by 7 exactly 12 times, both 84 5 and 84 0 fall through to no exact division to report, and hello never gets past the first question. That 84 0 run is where && earns its keep, because dividend % divisor with a zero divisor is undefined behaviour and the expression is sitting right there in the condition. It never runs. && and || evaluate their left operand first, and evaluate the right one only if the answer is still in doubt: && stops at a zero on the left, since nothing on the right can rescue it, and || stops at a nonzero. This is short-circuit evaluation, and the ordering is guaranteed by the standard rather than being one compiler's convenience. That distinction lands right after the previous lesson, which showed that the order of a function's arguments is unspecified, so printf("%d %d\n", i++, i) has no answer at all. && and || are the exception: each is a sequence point, and the left side is fully evaluated, side effects included, before the right side is looked at. Guarding a dangerous operation with && is a promise the language keeps. Notice also what the chain gave up, though. Two separate if statements each printed their own message, while the single && condition merges two different problems into one report. Combine conditions when the response is the same, and keep them apart when it is not.

Two Ways to Write an if That Lies

The next program is wrong, twice, and both mistakes compile.

#include <stdio.h>

int main(void)
{
    int score = 0;

    int converted = scanf("%d", &score);

    if (converted = 1)
    {
        printf("read a score\n");
    }

    if (score > 100);
    {
        printf("score is above 100\n");
    }

    return 0;
}

Given the input 5 it prints read a score and then score is above 100. The first condition is converted = 1, an assignment, and assignment is an expression whose value is the value assigned, so the line stores 1 into converted, hands 1 to the if, and is true on every run no matter what scanf did. The second if ends in a semicolon, and that semicolon is the body: an empty statement, executed conditionally, doing nothing. The block underneath is then an ordinary block that always runs, which is why a score of 5 gets announced as above 100. gcc catches both:

branch.c:9:9: warning: suggest parentheses around assignment used as truth value [-Wparentheses]
    9 |     if (converted = 1)
      |         ^~~~~~~~~
branch.c:14:21: warning: suggest braces around empty body in an 'if' statement [-Wempty-body]
   14 |     if (score > 100);
      |                     ^

It adds a third warning as well, this 'if' clause does not guard... [-Wmisleading-indentation], pointing at the block that only looks guarded. Both fixes are one character. if (converted == 1) compares instead of assigning, and deleting the stray semicolon lets the following block become the body it was always indented to be. Neither warning is on by default, which is the whole case for -Wall -Wextra in a sentence.

The switch Statement

When one integer is checked against a handful of fixed constants, a chain of else if says the same thing over and over. switch says it once: it evaluates the value, jumps to the matching case label, and runs from there.

#include <stdio.h>

int main(void)
{
    int level = 3;

    switch (level)
    {
        case 3:
            printf("debug detail\n");
            /* falls through */
        case 2:
            printf("warnings\n");
            /* falls through */
        case 1:
            printf("errors\n");
            break;
        default:
            printf("level %d is not a reporting level\n", level);
            break;
    }

    return 0;
}
debug detail
warnings
errors

"Runs from there" is the whole of switch. A case is a label, not a box, so control enters at the match and carries on through every following case until something stops it. break is what stops it, and forgetting one is the classic switch bug. Set level to 2 and the debug line disappears, set it to 1 and only errors prints, and set it to 9 and the default line reports that 9 is not a reporting level. This program has one break per outcome and deliberately falls through twice, because a level that reports debug detail should report warnings and errors as well, and the fallthrough expresses that nesting for free. Deliberate is the operative word: -Wextra warns this statement may fall through [-Wimplicit-fallthrough=] on any case that runs code and then flows into the next, and the /* falls through */ comment is exactly what silences it. The compiler reads that comment, so it is a claim you are making rather than decoration.

Two more rules. Always write a default, even when it only reports that the value was unexpected, because the alternative is a switch that silently does nothing for input nobody thought about. And the labels must be integer constant expressions: case 3: is fine, case level: is not, and neither is a case on text, since comparing strings needs a function call and a chain of if. That restriction is also why a chain testing ranges stays a chain, as switch can only ask whether a value equals a constant.

Key Takeaways

  • The comparison operators ==, !=, <, >, <= and >= each produce an int that is 1 when the relation holds and 0 when it does not, and if runs its block when the value is nonzero. Always brace the body, and write the comparison out rather than leaning on a value being nonzero.
  • Check scanf's conversion count, and check a divisor before dividing. Undefined behaviour cannot be detected after the fact, so the guard has to come first.
  • return in main ends the program, with 0 for success and nonzero for failure, which makes return 1; the way out of an error branch.
  • &&, || and ! combine conditions, and && and || short-circuit: the left operand is evaluated first and the right one only if it can still change the answer. That ordering is guaranteed, unlike the unspecified order of function arguments, which is what makes divisor != 0 && dividend % divisor == 0 a safe guard.
  • if (x = 5) assigns and is therefore always true, which gcc reports as suggest parentheses around assignment used as truth value. A stray semicolon as in if (x > 100); makes the body empty and the block below it unconditional, which gcc reports as -Wempty-body. Neither warning appears without -Wall -Wextra.
  • switch compares one integer against integer constant case labels and then runs from the match onward. End every case with break or an explicit /* falls through */ comment, which -Wimplicit-fallthrough checks for you, and always provide a default.