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, anamespace
,enum
, orclass
.to group statements that are part of another statement, as with an
if
orfor
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:
#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:
#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:
#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.
#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;
}