7. Week 4 Tuesday: Loops
≪ 6. Week 3 Thursday: If Statements | Table of Contents | 8. Week 4 Thursday: Algae Simulator ≫Prior to discussing loops, our primary way of making our programs interactive was by asking the user for some inputs (sometimes) and then having some branching logic involving if statements. These programs tend to be underwhelming, and anything more sophisticated was out of reach. Even a simple word game like hangman is out of reach. Thinking through the pseudocode very loosely, we’ll have to include a command like “Repeat until the user has run out of misses…” Moreover, we’ll have to “compare the user’s guess with each letter in the secret word”, etc. If statements alone simply aren’t enough.
The problem is that commands involving repetition aren’t possible with if statements alone (sort of, but we don’t talk about goto). This is of course exactly what loops help us do.
These come in two main flavours. First, there’s while loops, which address commands like “repeat while some condition is met” or “repeat until some condition is met”. The syntax is
C++ interprets this as the following set of commands:
- Check if
conditionis true. - If yes, execute the code and go back to step 1.
- If no, end the loop.
That’s it! Draw a flowchart if these three steps are confusing.
What’s most important is that C++ will check the condition before each iteration of the loop; forgetting about this order of events is a common source of mistakes.
The other flavour of loops are for loops, which address commands like “repeat for every character in this string” or “repeat for every integer \(1, 2, \ldots, n\)” (which is equivalent to repeating something \(n\) times). The for loop’s syntax is more complicated and is comprised of four parts:
Everything in the for loop is an expression, and I have made the semicolons between them clear. C++ interprets the above as follows:
- Execute the
setupcommand once. - Check if the
conditionis true. - If it is, execute the
codein the body, then execute theupdatecommand and return to step 2. - If it isn’t, end the loop.
Again, a flowchart may be a better depiction of this process.
Remark 1.
You may notice that the “flowchart” for for loops
is extremely similar to the “flowchart” for while loops,
i.e. it’s the same with a bit of extra spice.
Indeed, every for loop can be rewritten as a while loop!
is functionally the same code as
While these flowcharts can seem somewhat clunky and unnatural, there are some parallels to pseudocode that we can make:
- The pseudocode “Repeat
commandwhileconditionis true” corresponds to the C++ code - The pseudocode “Repeat
command\(N\) times” corresponds to the C++ code Replace \(N\) with whatever the number is; this can be a variable! Note also that there is a strict inequality…think about why. - The pseudocode “Repeat
commandfor each index \(i = 0, 1, \ldots, N - 1\)” corresponds to the C++ code This is typically used in conjunction with strings when one wants to perform an operation on each character.
That was quite a bit of text, so let’s look at an example.
Example 2. Censorship
Write a C++ program that allows a user to enter a line of text.
Then, replace every number in the user’s input with an asterisk *
and print out the modified text.
Sample Run:
INPUT I live at 123 RAM Street.
OUTPUT I live at *** RAM Street.
Let’s write some pseudocode first to describe what we want to happen.
- Accept a line of text from the user’s input
and store it in a string variable
line. - For each character
cin the user’s input, repeat the following:- If
cis a digit, replace it with an asterisk. - Otherwise, do nothing.
- If
- Print out the modified string.
But how can we use loops to do something
for every character c in a string?
At some point,
we’re going to have to use the .at() function for strings,
and we’ll be repeating some operation on line.at(0),
line.at(1), etc.
So, we ought to rephrase our pseudocode
in terms of the index rather than the character:
- Accept a line of text from the user’s input
and store it in a string variable
line. LetNbe its length. - For each index
i = 0, 1, 2, ..., N - 1, repeat the following:- Set
cto the character at indexi. - If
cis a digit, replace it with an asterisk. - Otherwise, do nothing.
- Set
- Print out the modified string.
Let’s convert this to code step by step.
For the first step, we need to be sensitive to spaces,
so we’ll use getline rather than a cin.
For the second step, we’ll set up a for loop
and write out the code according to the template earlier:
1for(int i = 0; i < N; i++) {
2 // get the character at index
3 char c = line.at(i);
4
5 // if it's a digit, replace it!
6 if(c >= '0' && c <= '9') {
7 line.at(index) = '*';
8 // why doesn't "c = '*';" work?
9 }
10
11 // otherise, do nothing (no code)
12}
And of course, at the end, we’ll need to print something out.
1cout << line << endl;
Common Mistake 3. Indexing by 0 vs. Indexing by 1
Note that on step 2,
we ensured that the indices started at 0 instead of 1,
and that the indices ended at N-1 instead of N.
While it’s more natural in common language
to describe the “first” letter rather than the “zeroth” letter,
such off-by-one errors can cause logical and runtime errors.
Let’s take a look at another similar example.
Example 4.
Write a C++ program that allows the user to input their full name. Then, remove any characters that aren’t spaces or letters, and convert their name to all caps. Print out the fully capitalised name.
Sample Run:
INPUT John Smith
OUTPUT JOHN SMITH
INPUT John Old McDonald
OUTPUT JOHN OLD MCDONALD
INPUT John O'Niel McDonald, Sr.
OUTPUT JOHN ONIEL MCDONALD SR
As usual, we’ll begin by producing some pseudocode. The first approach we’ll demonstrate is one where we create a fresh empty string, then append alphabetic characters from the user’s input one at a time (capitalising if necessary) and skipping over the non-space non-alphabetic characters. More specifically,
- Take a string
full_nameas input, and letNbe the length of the input. - Create an empty string called
all_caps. - For each index
i = 0, 1, ..., N-1, perform the following:- Let
chbe the character at indexiin the user’s input. - If
chis a space or an upper-case letter, append it toall_caps. - If
chis a lower-case letter, convert it to upper-case, then append it toall_caps. - Otherwise, don’t do anything with
chand keep going.
- Let
- Print out
all_capsto the screen.
Let’s now convert this to code.
We’ll be reliant on
ASCII math
and writing loops to do so.
First, to take a full name as an input (which may include spaces),
we should use getline rather than a direct cin statement.
Next, we’ll create the empty string.
1string all_caps = "";
For the loop, we’ll elect to use a for loop:
we are repeating an instruction for each index of a string again.
To append a character to a string,
you can either use the push_back function or the += operator.
1for(int i = 0; i < N; i++) {
2 // obtaining the character at index i
3 char ch = full_name.at(i);
4
5 // space or uppercase character: just add it to all_caps
6 if(ch == ' ' || (ch >= 'A' && ch <= 'Z')) {
7 all_caps += ch;
8 }
9
10 // lowercase character: convert to uppercase, then add.
11 if(ch >= 'a' && ch <= 'z') {
12 ch += 'A' - 'a';
13 all_caps += ch;
14 }
15
16 // remaining cases: just ignore them and don't do anything.
17}
The last step, of course,
is to print out all_caps to the screen. here’s the completed code:
Full Solution
1#include <iostream> // cin, cout
2#include <string> // getline, strings
3
4using namespace std;
5
6int main() {
7 // 1. read input, define N
8 string full_name;
9 getline(cin, full_name);
10 int N = full_name.length();
11
12 // 2. declare all_caps as empty string
13 string all_caps = "";
14
15 // 3. loop through the full_name string,
16 // one character at a time.
17 for(int i = 0; i < N; i++) {
18 // obtaining the character at index i
19 char ch = full_name.at(i);
20
21 // space or uppercase character: just add it to all_caps
22 if(ch == ' ' || (ch >= 'A' && ch <= 'Z')) {
23 all_caps += ch;
24 }
25
26 // lowercase character: convert to uppercase, then add.
27 if(ch >= 'a' && ch <= 'z') {
28 ch += 'A' - 'a';
29 all_caps += ch;
30 }
31
32 // remaining cases: just ignore them and don't do anything.
33 }
34
35 // 4. print out the result.
36 cout << all_caps << endl;
37
38 return 0;
39}
There is an alternative solution. It’s somewhat unnatural to define a new string and rebuild the user’s input one character at a time; I suspect more human pseudocode would be as follows:
- Take a string
full_nameas input and defineNas its length. - For each index
i = 0, 1, ..., N - 1, perform the following:- Define
chas the character at indexiinfull_name. - If
chis a lowercase letter, convert it to uppercase. - If
chis neither a space nor a letter, remove it from the string.
- Define
- Print
full_nameto the screen.
To remove a character from a string,
we can use the
erase
function for strings.
This pseudocode directly modifies the user’s input,
deleting unwanted characters
and capitalising lowercase letters as we go.
Let’s code this solution up:
1// 1. read full name from user input
2string full_name;
3getline(cin, full_name);
4int N = full_name.length();
5
6// 2. for each index 0, 1, ...., N - 1
7for(int i = 0; i < N; i++) {
8 // obtain character at index i
9 char ch = full_name.at(i);
10
11 // if ch is lowercase, convert to uppercase
12 if(ch >= 'a' && ch <= 'z') {
13 // note: we should not modify ch, since it's a copy
14 // of the character in the string full_name.
15 full_name.at(i) += 'A' - 'a';
16 }
17
18 // if ch is NOT a space and NOT an uppercase letter,
19 // delete the character from the string
20 if(ch != ' ' && (ch < 'A' || ch > 'Z')) {
21 // erases one character, starting at index i.
22 full_name.erase(i, 1);
23 }
24
25 // in remaining cases, do nothing!
26}
27
28// 3. print out results.
29cout << full_name << endl;
Problem 5.
Identify the two runtime and/or logical errors in the above code.
The two problems in the code are as follows:
first, the length of the string may decrease throughout the for loop.
If we ever end up deleting a character,
i = N - 1 would cease to be a valid index,
resulting in a runtime error.
The fix to this is to remove line 4,
and replace the condition in the for loop
with i < full_name.length().
This tells the for loop
to check the length of the string at every iteration,
making it robust to changes in string length.
The second problem is more subtle,
and it’s revealed when we input john o'neil.
The output becomes JOHN OnEIL;
the for loop somehow skips over the n following the apostrophe.
The reason is that when i = 6,
we delete the apostrophe and update the index to i = 7.
But when we delete the apostrophe, the n moves to index i = 6!
This index shifting causes the for loop
to skip over the n and leave it as-is.
The fix is to add i-- after line 22
to adjust the index according to this backwards shift.
This way, none of the characters get skipped inadvertently.
Warning 6. Avoid Adding or Removing Characters from Strings During Loops
As the above example demonstrates, even though directly modifying strings is a very human process, it is surprisingly delicate and subtle in implementation when we are adding or removing characters. As mentioned, this changes which characters have which index, and it gives us programmers a larger cognitive load to deal with.
This is not to say that such operations are entirely forbidden; in fact, iterators are sometimes designed with exactly these indexing issues in mind.