3.5.1. For-Loops

Time: 00:05:23 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | Slides (PDF)

C++ supports two kinds of for-loops. It derives its traditional for-loop syntax from the original K&R C and has changed little since. The for-range syntax is much newer; the ANSI C++ standard adopted it in 2011. The syntax of both versions shares the for keyword and the parentheses as shown below. Although both versions are detailed below, the traditional syntax is used more heavily throughout the text as the for-range relies on concepts and data types that we won't study until later chapters.

The Loop Control Variable

The behavior of for-loops, with some minor exceptions described below, is determined by a loop control variable. (It's possible but rare for traditional for-loops to have multiple control variables.) The variable has an initial and a terminal value, and it (usually) takes on a different value during each iteration of the loop. The next figure illustrates loop control variables and describes their scope.

for (int counter = 0; counter < 100; counter++)
	cout << counter << endl;

int counter = 0;
for (; counter < 100; counter++)
	cout << counter << endl;
(a)(b)
Loop control variables. The examples highlight the loop control variable in red.
  1. The loop control variable is defined inside the loop, which restricts its scope to the loop. So, the control variable goes out of scope when the loop ends1. Restricting the scope of the loop control variable reduces the possibility of "name collisions" (inadvertently using the same variable names in different contexts), making it easier for programmers to write correct for-loops. The C programming language does not allow programmers to define the loop control variable inside for-loops, making this syntax extension the primary change going from C to C++.
  2. The loop control variable is defined outside and above the loop, so the variable's scope extends from the definition to the end of the enclosing block.
Version (a) is the most common way to implement a for-loop in a C++ program.

Traditional For-Loops

One of the advantages of for-loops is their compact notation. They gather three control features into one location, making understanding the loop behavior relatively easy. Each for-loop consists of three parts:

  1. init: This statement (usually) defines the loop control variable in loop-scope and initializes it: int i = 0;. If necessary, programmers can define the loop control variable in a higher scope but initialize it in the for-loop each time the program enters the loop.
  2. test: This Boolean-valued expression usually compares the value stored in the loop control variable with some other value; the loop continues as long as this expression evaluates to true:
  3. update: this statement updates the loop control variable in such a way that the test will eventually become false:

The three parts appear between parentheses and are separated by required semicolons. The next figure illustrates the basic pattern of the for-loop and its logical behavior.

for (init; test; update)
	statement;
 
 
 
 
for (init; test; update)
{
	statement-1;
	    . . . 
	statement-n;
}
A logic diagram illustrating the execution path through a traditional for-loop. The program enters the loop at the initialization statement, which runs exactly once. Then, it evaluates the test; if the test is false, execution leaves the loop. If the test is true, the loop body runs, then the update runs, and the loop returns to the test.
(a)(b)(c)
Basic for-loop behavior. The for-loop runs the "init" statement exactly once, but it runs the "test" expression and the "update" statement once during each iteration of the loop. For-loops are a test at the top loop, meaning the test occurs before the program enters the body. So, if the test is initially false, the program skips the loop; otherwise, the loop runs while the test continues evaluating to true.
  1. A single-statement for-loop
  2. A for-loop with a compound or block statement
  3. A for-loop logic diagram

Infinite For-Loops

for (;;)
{
	statements;
	if (condition)
		end the loop;	// examined in detail later
}
An infinite loop. Infinite loops don't terminate normally - their condition or test always remains true. Sometimes infinite loops are caused by a malformed condition and represent a logical error. Other times, they are created deliberately and are useful when implementing daemons or other unending programs. They are also useful when implementing logic where it is more convenient or efficient to break out of the loop from within the body. Each of the three expressions appearing in the basic for-loop is optional - we may omit any or all. However, both semicolons are required. If we omit the middle expression or test, the loop becomes infinite and never ends.

Comma Operator

int	i;
int	j;

for (i = 0, j = 100; i < 50 && j > 0; i++, j--)
{
	// body of loop
}
The comma operator. The comma operator splices two statements together to make a single statement. It's rarely used outside for-loops in C++ programs and unusable outside for-loops Java programs. Programmers typically use it to initialize or update multiple variables in a single statement.

Other uses of the comma operator are possible in C++. However, they can be error-prone (as illustrated next), and experienced programmers generally consider using it outside a for-loop as a poor programming style.

int function()
{
	int x;
	int y;
		...
	return x, y;
}
Comma operator errors. Unfortunately, the comma operator also makes possible subtle, albeit naive, errors. The comma operator can also join multiple expressions, and the value of the overall expression is the value of the last expression in the list. Functions, chapter 6, can return a single value. Sometimes, novice programmers try to circumvent this restriction with the comma operator. The code fragment illustrated here compiles and runs but returns the single value stored in y.

For-Loop Examples

for (int i = 0; i < 10; i++)
	cout << i << " ";
for (int i = 0; i > 10; i++)
	cout << i << " ";
0 1 2 3 4 5 6 7 8 9  No output
(a)(b)
Basic for-loop examples. We must carefully examine all three parts of a for-loop to understand its behavior. First, where does the loop begin? That is, what is the initial value of the loop control variable? Second, where does the loop go? More formally, under what conditions does the loop continue to iterate, and what condition causes the loop to end? Whenever there is a problem with a for-loop, this is the first place we should look for an error. Finally, which direction does the loop run, up or down, and by how much? Usually, for-loops increase by one (formally, the program generally increments the loop control variable by one), but other options are possible. The increment operation is the second most common source of for-loop errors.
  1. The loop control variable is initialized to 0 and is s incremented by 1 during each iteration of the loop. So, the loop continues until i becomes 10. Using the loop control variable in the for-loop body is very common.
  2. For-loops are a test at the top loop, which means that the program evaluates the loop condition or before the body of the loop runs. If, as in this example, the condition is initially false, the loop body never runs.
for (int i = 0; i < 10; i += 2)
	cout << i << " ";
for (int i = 10; i > 0; i--)
	cout << i << " ";
0 2 4 6 8  10 9 8 7 6 5 4 3 2 1 
Variations on counting. We use so many for-loops that count up by one that sometimes we forget that for-loops are capable of greater variety. They can count down just as well as they count up, and because the update operation can be any valid statement, they can count in any way we want. The following examples are still pretty simple but illustrate different counting techniques.
  1. The update statement, highlighted in yellow, increments the loop control variable by two during each iteration. So, this loop iterates over even, non-negative values from 0 to 8. An easy error is to use i+2 in place of the highlighted statement, but that expression doesn't change i, and the loop doesn't work.
  2. This loop counts backwards by 1 from 10 to 1. What changes are needed to make it count from 9 down to 0?
for (int row = 0; row < 5; row++)
{
	for (int col = 0; col < 4; col++)
		cout << "[" << row << "][" << col << "] ";
	cout << endl;
}
[0][0] [0][1] [0][2] [0][3]
[1][0] [1][1] [1][2] [1][3]
[2][0] [2][1] [2][2] [2][3]
[3][0] [3][1] [3][2] [3][3]
[4][0] [4][1] [4][2] [4][3]
Nested for-loops. It's often convenient to think about data having a "shape" or "dimensions." For example, in the previous figure, we counted either up or down, and the program displayed the resulting numbers on a single line or dimension. In this example, we want to display numbers over multiple columns and rows in two dimensions. A simple rule of thumb is to use a for-loop for each dimension. This example highlights the outer loop in yellow and the inner loop in blue.
  1. The outer for-loop initializes the loop control variable, row, to 0
  2. If row is less than 5, the outer loop executes (control goes to step 2.a). When row equals 5, control goes to step 3
    1. The inner loop initializes the loop control variable col to 0
    2. If col is less than 4, the inner loop executes (control goes to step 2.b.i). When col equals 4, control goes to step 2.c
      1. The inner cout statement prints row and col, formatted as illustrated (but it does not print a new line - the output continues on the same line)
      2. The value in col is incremented (note that the value in row is not changed)
      3. Control goes to step 2.b
    3. The outer cout statement prints a new line, which ends the line and moves the cursor back to the left edge of the console window
    4. The value of row is incremented
    5. Control goes to step 2
  3. The loop ends and the program runs the next statement following the outer for-loop
If neither loop is interrupted, then nesting the loops has the effect of multiplying the iterations of both loops. The outer loop executes 5 times, and the inner loop executes 4 times for each iteration of the outer loop. So the body of the inner loop is executed 5 × 4 = 20 times.

For-Range Loops

For-range loops are a relatively new addition (2011) to C++ and parallel similar constructs available in C# and Java (where they are called for-each loops). A for-range loop iterates over the individual elements of a range. The next figure illustrates the basic pattern of the for-range loop and its logical behavior.

for (definition : range)
	statement;

Or

for (definition : range)
{
	statement-11;
	    . . . 
	statement-n;
}
A logic diagram illustrating the execution path through a for-range loop. The program enters the loop at the end-of-range test. If the test is true, the execution leaves the loop. If the test is false, the next element in the range is copied to the loop control variable, the loop body runs, and the loop returns to the test.
For-range loops. Each element stored in the "range" is copied, one at a time, in data order, from the range and stored in the loop control variable. The loop terminates after the last element is copied and processed. For-range loops are a test at the top loop: if the range is initially empty, the loop never executes.

The range is an ordered data structure that typically contains multiple elements (e.g., the characters in a string). The definition creates a loop control variable with scope restricted to the for-range loop. The loop copies the first element from the range to the control variable during the first iteration, the second element during the second iteration, and so on, saving a subsequent element from the range in the control variable during each iteration. If the range is a string, then the definition creates a character variable: char c, and if the range is an array, then the definition creates a single variable of the type stored in the array. C++ allows three types of ranges:

  1. Strings (instances of the string class, introduced in Chapter 8)
  2. Arrays (in a limited context - introduced in Chapter 7)
  3. Containers and especially instances of the STL (the standard template library) classes

Specifically, any iterable data type (i.e., data that supports the begin and end functions) may be used as the range. Unfortunately, what data types satisfy this requirement is confusing, as illustrated by the following examples.

For-Range Examples

While for-range loops are not inherently difficult to understand, the range consists of data types or structures the text hasn't yet introduced. Some of the following examples rely on your experience in CS 1400, while others are, at least for now, just bits of syntactic "magic." Just focus on the syntax of the for-range control structure, knowing we will discuss strings, arrays, and the rest later. The first example demonstrates a for-range loop looping over each character in a string. The isalpha function returns true if its argument is an alphabetic character (i.e., a-z or A-Z).

Works Works Fails
string s =  "Hello, World!";
for (char c : s)
    if (isalpha(c))
        cout << c << " ";
for (char c : "Hello, World!")
    if (isalpha(c))
        cout << c << " ";
 
char* s = "Hello, World!";
for (char c : s)
    if (isalpha(c))
        cout << c << " ";
(a)(b)(c)
 
H e l l o W o r l d
 
(d)
For-range loops with string data. "Hello, World!" is a string constant or string literal; it is also a C-string, which we learn about in Chapter 8.
  1. s is an instance of the C++ string class that is initialized to the characters in the string literal. The string class supports the begin and end functions.
  2. Although we don't see the code that carries out the operation, the string literal "Hello, World!" is converted into an anonymous or unnamed instance of the C++ string class, and the loop runs just as in example (a)
  3. In this example, variable s is a C-string, which does not support the begin and end functions and, therefore, may not be used with a for-range loop
  4. The output produced by the two working examples

In the context of for-range loops, arrays (chapter 7) are very problematic: they work in a very narrow context but generally fail, as is demonstrated next:

Works Fails
char cs[] = "Hello, World!";
for (char c : cs)
	cout << '[' << c << ']';
void print(char a[])
{
    for (char c : a)
        cout << '[' <<  << ']';
}
    . . .
char cs[] = "Hello, World!";
print(cs);
(a)(b)
int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
for (int i : a)
	cout << '[' << i << ']';
void print(int a[])
{
    for (int i : a)
        cout << '[' << i << ']';
}
    . . . 
int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
print(a);
(c)(d)
[H][e][l][l][o][,][ ][W][o][r][l][d][!][ ]
[1][2][3][4][5][6][7][8][9][10]
 
(e) 
For-range loops with arrays. In the failing examples, print is a function (Chapter 6), which is very much like a Java method.
  1. Looping over the elements in a C-string created as an array. "Hello, World!" is transparently converted into an instance of the string class at compile time, which is iterable
  2. Passing the character array as a parameter to a function causes the code to fail. At compile time, there is not enough information to create a string object in the function and a[] remains a C-string, which is not iterable
  3. Looping over the elements in an array of integers. The array a is transparently converted (at compile time) into a C++ vector, which is iterable
  4. Just as in example (b), passing an array as a function parameter doesn't work with a for-range loop as there is insufficient information to create a vector at compile time.
  5. Output from the working examples. What is the last, seemingly invisible character (highlighted) output at the end of the first working loop? The answer concerns the "secret sauce" that makes a character array into a C-string and is revealed in Chapter 8.


1 The first C++ compiler, cfont, implemented a different scoping rule: A loop control variable defined inside the for-loop remained in scope until the end of the block. So, the syntax of version (a) had the meaning of version (b). At a conference I attended in 1990, someone asked Stroustrup about the scope of the loop control variable. He agreed that the meaning was wrong but doubted that it would change. Stroustrup's comments notwithstanding, the language was modified as described in Figure 1.