7. Week 4 Tuesday: Practise with Loops!
≪ 6. Week 3 Thursday: getline, If Statements, and Conditionals | Table of Contents | 8. Week 4 Thursday: Break, Continue, and Some Common Errors ≫So far, we’ve learned a lot about coding: we’ve learned how to produce outputs, accept user inputs, store and manipulate variables, and use if statements to produce branching, responsive behaviours. However, this is still a rather limited set of capabilities, and we’re far from capable of utilising C++’s full power.
What if you’re printing out all of the numbers between 1 and 1000, for whatever reason? There’s got to be a better way to do this than just typing cout << 1 << " " << 2 << " " << ....
. Moreover, what if you’re printing all the numbers between 1 and 10000? Or even worse?
While this situation is quite contrived, there are many more situations where you need to repeat code a known or unknown number of times, such as:
- You’re collecting data from an electronic thermometer. You’ve harvested two million data points, but you need to convert the data from Celsius to Fahrenheit.
- Sub-scenario: you’re interested in statistics related to this data, such as the mean, standard deviation, how well it fits a model, etc.
- You’ve scanned a book into your computer, and you need to count how many words are in that book.
- You need to compute a mathematical expression that’s defined using sums, products, or a recursive relationship — examples include computing \(n!\), computing binomial coefficients, computing elements of the Fibonacci sequence, etc.
- A related sub-scenario is if you need to apply Newton’s method to numericall solve a differential equation (or, more commonly, a higher-order numerical solution such as the Runge-Kutta methods).
- You’re sorting your (totally legal) digital library of thousands of movies alphabetically.
There are a billion other scenarios that revolve around repeating instructions some number of times or until some condition is reached. As a mathematician, most of my examples will arise from data analysis and numerical methods, but I hope it’s enough to convince you nonetheless.
We can delineate loops into two separate categories: either you are repeating a set of instructions until something happens, or you are repeating a set of instructions a known number of times. We can describe the first situation as a while
loop, and the syntax is as follows:
This will check the condition next to the while loop, then execute the code between the curly braces if the condition was true. Once the code is executed, it will go back to the top and check the condition again. If at any time the condition is false, it will skip everything between the curly braces and move on.
In fact, while loops can be used to capture the second situation as follows:
1int repetitions = 0;
2while(repetitions < [number of repetitions]) {
3 // repeated code...
4 repetitions++;
5}
This will keep track of how many times the code has been repeated. It will repeat the code until it’s been repeated an appropriate number of times.
This is bad for two reasons:
- You need two extra lines to keep track of the number of repetitions, and they may be spread out over your code. This makes the code far less manageable.
- The variable name
repetitions
in the above example can never be declared again. You will either have to re-use the same variable several times (and keep track of it throughout all of your code!) or come up with a new variable name for each loop.
The workaround is to use a for
loop, which has the following syntax:
This keeps everything in one place so it’s easier to keep track of, especially for when you need to modify your code. You may replace counter
with whatever name you want! Often times, we’ll use the name i
instead of counter
since it’s easier to write.
Very generally speaking, if you want to repeat code a known number of times, you should use a for loop. If you don’t know how many times you’ll need to repeat some code, you should use a while loop.
There are some things to pay attention to regarding for loops, however.
- Be wary of the semicolons! They only go between the three statements within the for loop, but not after the
counter++
. - The variable
counter
is accessible within the loop, but not outside the loop. Once the for loop has finished running, the variablecounter
will cease to exist, and you are allowed to redefine it (or even use it in a new for loop) if you want! - The
int counter = 0;
functions as a mostly regular variable declaration in C++. This means that if you have already defined the variablecounter
before, the code will break. - Like while loops, for loops check the condition before each repetition of the loop.
Common Mistake 1. Off-by-One Errors
Suppose, for whatever reason, that you need to print the string “I like cheese” to the screen five times. You write the code
However, you realise that when you run this code, it prints six lines instead of five! This is because the condition in the for loop is i <= 5
instead of i < 5
; this causes the variable i
to range over the values 0, 1, 2, 3, 4, 5
, i.e. it causes the print statement to repeat 6 times. This is very, very common and easy to overlook. Be very mindful of how many times your for loop repeats and which values your counter variable (in this case i
) is taking.
Remark 2. Unwrapping for loops
You may not need to know this for PIC 10A, but I think it provides some relevant information.
The syntax described above is not the broadest description. In fact, the proper syntax would be
This code is exactly the same code as
This illustrates exactly which order the statements inside the for
loop are executed in. It additionally allows you to write some very strange code that magically works, but I’ll omit that to avoid confusing you.
That was quite a bit of text, so let’s look at an example.
Example 3. 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
c
in the user’s input, repeat the following:- If
c
is 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
. LetN
be its length. - For each index
i = 0, 1, 2, ..., N - 1
, repeat the following:- Set
c
to the character at indexi
. - If
c
is 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 since we know something is repeated exactly N
times! So,
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 4. 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 5.
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_name
as input, and letN
be the length of the input. - Create an empty string called
all_caps
. - For each index
i = 0, 1, ..., N-1
, perform the following:- Let
ch
be the character at indexi
in the user’s input. - If
ch
is a space or an upper-case letter, append it toall_caps
. - If
ch
is a lower-case letter, convert it to upper-case, then append it toall_caps
. - Otherwise, don’t do anything with
ch
and keep going.
- Let
- Print out
all_caps
to 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 know exactly how many times the loop should run (N
times). 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_name
as input and defineN
as its length. - For each index
i = 0, 1, ..., N - 1
, perform the following:- Define
ch
as the character at indexi
infull_name
. - If
ch
is a lowercase letter, convert it to uppercase. - If
ch
is neither a space nor a letter, remove it from the string.
- Define
- Print
full_name
to 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 6.
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 7. 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.