Loops and If-Tests

reading

Cyganek section 3.13

We’ve been using if tests and for loops already, but now it’s time to understand some of the nuances of these statements.

Statement Blocks

We use curly braces, {} to denote blocks of code. These are used in a number of contexts:

  • to define the body of a function (so far we’ve only seen main(), but this applies to any function).

  • to define the members of a struct, or as we’ll soon see, a namespace, enum, or class.

  • to group statements that are part of another statement, as with an if or for loop.

One thing to keep in mind with these blocks is scope—this means whether we have access to the value held by an object.

A general rule is that we can access objects that are defined outside of our current scope.

Let’s look at a Fibonacci example:

Listing 20 fibonacci.cpp
#include <iostream>
#include <vector>

int main() {

    // note that the limits of int are 2147483647, so we are going to overflow
    // after ~50 terms

    // we could instead use long here

    std::vector<int> fib{0, 1};

    int n{0};

    std::cout << "how many terms should we compute?" << std::endl;
    std::cin >> n;

    for (int i = 2; i <= n; ++i) {
        double f_new = fib[i-1] + fib[i-2];
        fib.push_back(f_new);
    }

    for (auto e : fib) {
        std::cout << e << " ";
    }
    std::cout << std::endl;

}

Inside of the main() function, we create a vector called fib and an integer n. Both of these are in scope inside of main—that means that we can use them and access them as needed.

Note

One of the nice things about the standard types like vector is that C++ automatically cleans up their memory when they go out of scope. This occurs at the last } of main(). At that point, fib is destroyed and its memory is freed. The same happens with n.

This behavior is called an automatic variable.

Automatic variables are allocated in a special part of memory called the stack. When you enter a function, all of the automatic variables are pushed down onto the stack and are available as long as they stay in scope. When you exit the function, the variables are popped off the stack and the memory is automatically freed.

In our example above, there are a few stacks. The outermost static is defined by the {} for main().

Note

Stack space is limited and you can run out, resulting in a stack overflow.

Later we’ll see that we can manually manage memory for variables that we place in the heap which is generally much larger.

(But also note that a vector keeps the storage for its elements separately from the vector object itself, and those can be on the heap and automatically managed for us.)

The for loop defines a new scope, and when we enter the {} delineating the loop block, a new stack is created, where the variables local to that block are managed. When we exit the loop, that stack is destroyed, freeing up all of the memory those local variables needed.

Shadowing

Consider the following:

bool test{true};
double x{0.0};

x = 3.14;

if (test) {
    double x = 1.0;
}

Here we’ve defined a new variable x inside the if block. Since it has the same name as the object x in the main code, we say that we are shadowing the value from outside our block. In general, this should be avoided. These two x objects are completely separate in memory and once we left the if block, the inner x is destroyed and its contents are not available to us. However, someone reading the code might not realize this.

Tip

g++ can warn you about shadowing if you include the -Wshadow option to the compiler.

Conditional Statements

if-test

We’ve already been using if-tests quite a bit. So let’s look a little more at their syntax:

if (condition1) {
   // do things if condition1 is true

} else if (condition2) {
   // do things if condition1 is false but condition 2 is true

} else {
   // do things only if both condition1 and condition2 are false

}

Tip

There is a form of an if-statement that does not use brackets if there is only a single statement to execute:

if (condition)
    statement;

This is potentially dangerous—if someone later edits the code and decides that they want to add another statement to that condition, they might do:

if (condition)
    statement;
    another_statement;

But since there are no braces, only statement is conditionally-executed. another_statement is not part of the if-test and will always be executed.

For this reason, it is always best to use brackets.

Note

C++17 also allows for a form with an initializer before the conditional (e.g., to open a file). We will not explore this here.

Tip

There is also a simple ternary operator in C++ of the form:

condition ? true-result : false result

Where true-result is the value used if condition is true and false-result otherwise.

For instance:

int i{10};

double x = (i > 5) ? 1.0 : 0.0;

switch statement

A switch statement takes action on a single expression, and has many different cases that can take different actions. For example:

int i{2};
std::string text{};

switch (i) {

   case 0:
       text = "zero";
       break;

   case 1:
       text = "one";
       break;

   case 2:
   case 3:
   case 4:
       text = "2 <= i <= 4";
       break;

   default:
       text = "i > 4";

}

Notice that each case region ends with break. If you omit the break, then the flow “falls through” to the next options.

for loops

We’ve already seen the basic structure of a for loop:

for (initializer ; condition ; iterator) {
     // do stuff
}

We can do this for just a simple integer counter:

for (int i = 0; i < 10; i += 2) {
     // we'll see i = 0, 2, 4, 6, 8
}

or with an iterator, like:

std::string my_string{"this is my string"};

for (auto it = my_string.begin(); it < my_string.end(); ++it) {
     // work on the string character by character
}

Note

Just like with if, there is a single-statement form of for that doesn’t use brackets for the loop body—this should be avoided.

We also saw the range-for loop that works with a variety of containers. For example:

std::vector<double> x{0.0, 1.0, 2.0, 3.0};

for (auto e : x) {
    // work on the current element in x
}

while and do-while loops

There are two types of while loops in C++. The first takes the form:

while (condition) {
    // do stuff
}

where the body is executed so long as condition is true. For example:

int i{0};

while (i < 10) {
   i = 2*i;
}

The loop body is only ever executed if the condition is true. The other form puts the while at the end:

do {
    // do stuff
} while (condition);

In this case, all of the statements in the loop body are executed at least once.

Note

The do {} while (condition) form is discouraged.

Finally, you can loop over a range simply by using an initialization list:

Listing 21 list_loop.cpp
#include <iostream>

int main() {

    for (auto year : {2020, 2021, 2022, 2023}) {
        std::cout << "the year is " << year << std::endl;
    }

}

continue and break

Sometimes we want to exit a loop early or skip the remainder of the loop block if some conditions is met. This is where continue and break come into play.

It is not uncommon to write an infinite loop, like:

for (;;) {
    // do stuff
}

or

while (true) {
    // do stuff
}

In this case, we will want a way to bail out. Here’s an (silly) example of asking for a word from a user and telling them how many letters it contains. But if they enter “0”, we exit:

Listing 22 infinite_loop.cpp
#include <iostream>
#include <string>

int main() {

    std::string word{};

    while (true) {

        std::cout << "Enter a word (0 to exit): ";
        std::cin >> word;

        if (word == "0") {
            break;
        }

        std::cout << word << " has " << word.size() << " characters" << std::endl;
    }

}

continue is used to skip to the next iteration. Here’s an example that loops over elements of a vector but only operates on them if they are even numbers, in which case, it negates them.

Listing 23 continue_example.cpp
#include <iostream>
#include <vector>

int main() {

    std::vector<int> a{0, 4, 10, 3, 21, 100, 63, 7, 2, 1, 9, 20};

    for (auto &e : a) {
        if (e % 2 == 1) {
            continue;
        }
        e *= -1;
    }

    for (auto e : a) {
        std::cout << e << " ";
    }
    std::cout << std::endl;
}