Program flow: conditionals and loops

Thus far, we have discussed writing programs that communicate with the user and can do some basic math. In order to make our programs useful and interesting, they need to be able to make decisions. In this section we will discuss control flow which is how a program can alter which statements get executed and the order in which statements get executed based on specific conditions. Up until now, every program we have written has gone straight from top to bottom; this need not be the case.

As we discuss control flow, we will see that only two of these control flow statements are actually needed. However, trying to write an entire program with just if and goto is not recommended. C provides a variety of different control flow structures for different purposes. Often, you will find that there are several different ways you could implement a particular piece of programming logic. When deciding how to implement something there are a few things you should consider:

If Statements

This simplest control structure in any programming language is the if statement. In the following piece of code, the printf statement only gets executed if the value of x is negative:

if (x < 0) {
    printf("x = %d is negative!\n", x);
}

Notice the braces. When the computer encounters the if statement, it first evaluates the expression in the parentheses, called the condition. If the condition is nonzero, it executes the code in the block associated with the if statement.

We can also make if statements more complex. Any if statement can be followed by any number of else if blocks and a possible final else block. For example, if we really wanted to check x thoroughly, we could write:

printf("x = %d", x);
if (x < 0) {
    printf(" is negative!\n");
} else if (x > 0) {
    printf(" is positive!\n");
} else {
    printf(" is zero.);
}

First, regardless of the value of x, it gets printed to the screen. Then, if x is negative, printf is called to complete the sentence with ” is negative!“. If the x < 0 condition fails, it goes on to check the condition x > 0. If this condition succeeds, it makes a different call to printf. Finally, if neither x < 0 or x > 0 succeeds, it calls printf with is zero..

In an multi-part if statement like this, the conditions are evaluated from top to bottom. If any if (or else if) succeeds, the conditions for following else if statements are not even evaluated and the program skips to the bottom of the combined if statement. An if statement does not need a terminating else block but it can have at most one and it must be the final condition.

Loops

Another major type of control flow statement is a loop. A loop is a block of code that runs multiple times. Most programming languages, C included, have 3 basic types of loops: while, do-while, and for. We’ll start with the while loop.

Consider the following code:

int i;

i = 0;
while (i < 10) {
    printf("%d\n", i);
    i = i + 1;
}

The while statement is like an if statement only it executes the statements in its associated block repeatedly until the condition fails. In the above code, the variable i starts with a value of 0. Each time the loop runs, the value of i is printed to the screen and then incremented by 1. The loop runs as long the statement i < 10 holds true. On the 10th time through the loop, the value of i finally reaches 10 and the loop does not execute an 11th time.

The loop above runs exactly 10 times every time it’s executed. Usually while loops are used when you don’t know how many times the loop will need to run. For example, if you were approximating an integral, you might want to run the loop until the error is below a certain threshold. In that case error < 0.001 might be a realistic loop condition.

The do-while loop is basically the same as the while loop except that it checks the condition after the loop has been executed. This means that the statements in the do-while block will always get executed at least once regardless of the condition. The syntax for a do-while statement in C looks like this:

do {
    /* Approximate the integral */
} while (error > 0.001);

The third type of loop is the for loop. The for loop, conceptually, is for looping through a set of things. For example, you could loop through all the numbers between 0 and 9 like we did above, or loop through some sort of a list and do something with each item. A for loop in C is of the form:

for (<expression>; <condition>; <expression>)
{
    /* Loop Block */
}

Instead of simply having a single condition like a while loop, a for loop has two additional expressions. The first of expression is an initialization; it gets executed once before the loop begins. The second expression is a condition just like in a while loop. The third expression gets evaluated after everything in the block but before the condition. This allows you to make our first loop example much more compact:

int i;

for (i = 0; i < 10; i = i + 1) {
    printf("%d\n", i);
}

All of the expressions in a for loop are optional. If no condition is given, it simply loops forever or until you manually break out of it.

Jump Statements

C has four statements called jump statements. Jump statements allow your code to jump from one place to another in its execution. The first of these statements is the return statement. We have seen return before and will see it again when we talk more about functions. Simply put, the return statement causes your program to leave the current function without executing any more statements.

For working with loops, C provides the break and continue statements. The break statement causes the program to leave the current loop. Usually this is used if you want to leave a loop early; for example, you could break out of a for loop before it gets to the end. Suppose you wanted to add up all of the numbers between 1 and 100 but stop if it gets above 150. You could do that with a simple break statement:

int i;
int sum;

sum = 0;
for (i = 1; i <= 100; i = i + 1) {
    sum += i;
    
    if (sum > 150) {
        break;
    }
}

You could also implement basically the same thing by changing the condition to i <= 100 && sum <= 150. Using the break statement allows the for loop to conceptually mean “for every i between 1 and 100” makes the sum being above 150 a conceptual “stop condition”.

The continue statement causes your program to skip the rest of the statements in the loop block and go on to the next iteration. The following code uses a continue statement to add up all of the numbers between 1 and 100 that are not divisible by 3:

int i;
int sum;

sum = 0;
for (i = 1; i <= 100; i = i + 1) {
    /* If the number is divisible by 3, we don't care about it */
    if (i % 3 == 0) {
        continue;
    }

    /* This statement only gets executed for values of i that are not divisible by 3*/
    sum += i;
}

The final jump statement is goto. The goto statement allows you to jump to an arbitrary location in your function. Later in this course, we will see some good uses for goto. For now, I suggest you leave it alone as it usually just makes code a mess. Whenever you are tempted to just use a goto statement, you should probably ask yourself if there is a better way to do it; there probably is.

The switch statement

One of the more strange control flow statements in C is switch. The switch statement is best explained by an example:

char unit;
float temperature, celsius;

switch(unit) {
case 'C':
    celsius = temperature;
    break;
case 'F':
    celsius = (temperature - 32) * 5 / 9;
    break;
case 'K':
    celsius = temperature - 273.15;
    break;
default:
    celsius = 0;
    /* This is an error */
    break;
}

Instead of a condition, the switch statement takes an integer value. It then has a bunch of cases each labeled by a constant. If the value given to switch matches one of those cases, it goes to that case. If the value given to switch doesn’t match any of the cases, it goes to default. Technically, the default case is optional; if there is no default case, it simply goes to the end of the switch statement in unmatched cases.

Take note of the break statements. Once the program jumps to a case in the switch statement, it will continue to execute statements until it reaches a break. If you do not put a break as the last statement to a case, it will continue on and execute the statements in the next case. Sometimes this is very useful, but most of the time you should simply put a break at the end of each case.

An Example

Let’s end this discussion of control flow with a more advanced version of our temperature conversion program:

/* temperature.c: A temperature conversion program */

#include <stdio.h>

int main(int argc, char **argv)
{
    char units;
    float celsius;
    float input_temperature;
    int items_read;

    printf("Welcome to our temperature conversion program!\n");

    while (1) {
        printf("Please enter a temperature with units (Example: 32 C):");

        items_read = scanf("%f %c", &input_temperature, &units);

        if (items_read != 2) {
            /* There was an error reading the temperature */
            break;
        }

        switch (units) {
            case 'c':
            case 'C':
                celsius = input_temperature;
                break;
            case 'f':
            case 'F':
                celsius = (input_temperature - 32) * 5 / 9;
                break;
            case 'k':
            case 'K':
                celsius = input_temperature - 273.15;
                break;
            default:
                printf("Invalid units.\n");
                continue;
        }
        
        printf("That temperature is:\n");
        printf("    %.2f degrees Celsius\n", celsius);
        printf("    %.2f degrees Farenheight\n", (celsius * 9) / 5 + 32);
        printf("    %.2f Kelvin\n", celsius + 273.15);
    }
}