Control flow in a programming language context refers to the mechanisms by which actual execution can be controlled. C and C++ have control structures in each of the following categories: expression evaluation, sequencing, selection, iteration, and recursion.
Expression Evaluation has been discussed in previous chapters.
Sequencing refers to the most basic of control, in which the statements in a block are executed in the order in which they are encountered in the source code. We have discussed sequencing (without naming it) in a previous chapter.
Selection allows statements to be conditionally executed based on evaluation of some expression or other condition. C/C++ has mechanisms for implementing selection. The first implements the logic model and uses one or more statements of the family if, else, and else if. The second implements the enumerated cases model and uses the switch statement.
Iteration allows a block of statements to be executed repeatedly. There are three mechanisms implementing iteration in C/C++, all of which are informally called loops: for, while, and do while.
Recursion is implemented in C and C++ by the simple expedient of
allowing a function to call itself. We will discuss recursion in another
chapter.
We look briefly at selecton and iteration in the remainder of this chapter.
The if statement is used when one statement (which may be a compound statement in the form of a block enclosed in braces) should be executed only when a certain condition that is knowable only at run time is true. The syntax is as follows (where reserved words and required syntactical elements are show in color):
if ( condition ) // statement1 // statement2
The effect of this code is that statement1 is executed if and only if the condtion evaluates to "true" (or to a non-zero integeral value), followed by the unconditional execution of statement2.
Note 1: In this and the following examples, a condition is meant to be any expression that returns values of type bool or any other integral type. Recall that in C and C++, a non-zero integral value is interpreted as "true" and a zero integral value is interpreted as "false".
Note 2: The statements 1 and 2 may be compound, that is, several statements enclosed within braces. For example, statement1 might be the block
// compound statement1 { // statement1.1 // statement1.2 // statement1.3 }
so that the original code would look like this:
if ( condition ) { // statement1.1 // statement1.2 // statement1.3 } // statement2
Please keep in mind that all of the statements mentioned below may be compound,
that is, a block consisting of several statements (executed in sequence)
enclosed within braces.
The if and else statements are paired when there are two statements that are to be executed in "either/or" fashion, depending on whether a condition evaluates to true or false. The syntax is as follows (where reserved words and required syntactical elements are show in color):
if ( condition ) // statement1 else // statement2 // statement3
Here, statement1 is executed when the condition evaluates to true (or a non-zero integral value), statement2 is executed when condition evaluates to false (or a zero integral value), and then statement3 is unconditionally executed.
Again, note that statements 1 and 2 may be compound, so that the code might look more like this:
if ( condition ) { // statement1.1 // statement1.2 // statement1.3 } else { // statement2.1 // statement2.2 // statement2.3 } // statement3
When there are more than two statements that need to be executed depending on several conditions, there are two C/C++ solutions. The first uses else if along with if and possibly else.
Suppose for example that there are three statements and that it is desired to execute statement1 if condition1 is true, execute statement2 if condition1 is false but condition2 is true, and execute statement3 if both conditions fail, picking up sequential execution with statement4. Then the syntax would be as follows (where reserved words and required syntactical elements are show in color):
if ( condition1 ) // statement1 else if ( condition2 ) // statement2 else // statement3 // statement4
The compund statement version would look like the following:
if ( condition1 ) { // statement1.1 // statement1.2 // statement1.3 } else if ( condition2 ) { // statement2.1 // statement2.2 // statement2.3 } else { // statement3.1 // statement3.2 // statement3.3 } // statement4
The if .. else if... else structure can be expanded indefinitely to allow for many possible statements that are executed conditionally based on successive evaluation of corresponding conditions. For example, conditional execution of four statements would look something like the following code:
if ( condition1 ) // statement1 else if ( condition2 ) // statement2 else if ( condition3 ) // statement3 else // statement4 // statement5
where statement1 is executed if condition1 evaluates to true, statement2 is executed if (!condition1 && condition2) evaluates to true, statement3 is executed if (!condition1 && !condition2 && condition3) evaluates to true, and statement4 is executed if (!condition1 && !condition2 && !condition3) evaluates to true. statement5 is then executed unconditionally.
However, as the number of cases gets large, designing the conditional expressions and keeping track of code gets increasingly complex, making the code difficult to read and maintain. An alternative, that is both more readable and actually more efficient when translated to executable code, is the switch statement, whose syntax is as follows (where reserved words and required syntactical elements are show in color):
switch ( selector ) { case s1 : // statement1 break; case s2 : // statement2 break; case s3 : // statement3 break; case s4 : // statement4 break; case s5 : // statement5 break; default: // statement6 } // statement7
Note 3: a selector is meant to be an expression that returns values of some integral type. Specific values of the selector (the expressions s1..s5 in the example above) are used to divide execution into cases.
The switch statement syntactical elements may be used as illustrated to implement mutually exclusive execution: exactly one of the six statements 1..6 will be executed, followed by unconditional execution of statement7. By leaving off a break statement, flow through execution is performed, in which the code for the following case is also executed. Thus, for example, in the code below there are no break statements:
switch ( selector ) { case s1 : // statement1 case s2 : // statement2 case s3 : // statement3 default: // statement4 } // statement5
Once any case is entered, flow-through execution requires that the remaining statements be executed. For example, if execution enters at case s2 the statements 2, 3, 4 and 5 are executed sequentially.
The default case may be omitted. The effect is to specifically
enumerate all of the cases expected, with no "default" behavior
specified. However, it is often a good programming practice to use the default
case to detect errors in program logic. Under circumstances where all
valid cases are known, say s1, s2, s3, s4, then encountering
another value would represent an error of some sort in the code. An error
message can be coded in as the default case.
The C/C++ for statement is a very useful and flexible mechanism for implementing iteration. In almost all situations where a loop is needed, for is arguably the best choice. It is certainly the choice that should be considered first, and rejected for the more specialized while and do while only for good reasons that are specific to the code you are writing. We first present the syntax of for, with reserved words and required syntactical elements are show in color). Then we will discuss the various components one at a time.
for ( initializer_statement ; continuation_condition ; update_statement ) { // C++ statements }
The portion
for ( initializer_statement ; continuation_condition ; update_statement )
is called the loop header and the portion
{ // C++ statements }
is called the loop body. Note that the loop body is actually just a statement. We have illustrated the case where it is a compound statement. In the loop header, there are three items that play a role in any loop.
The initializer_statement may be any statement, and it is executed once as the loop is first encountered during sequential execution. The typical use of the initializer_statement is to initialize variables such as loop counters.
The continuation_condition is a condition statement that is executed at the beginning of each iteration of the loop. When it returns "true" (or non-zero), the loop body is executed. When it returns "false" (or zero), the loop is terminated and the program returns to sequential execution immediately past the end of the loop body.
The update_statement may be any statement, and it is executed after
each execution of the loop body. It is often used to increment, decrement, or
otherwise update loop control variables.
The while statement is used in more specialized circumstances, for example: (1) when there is no initialization required, or perhaps (2) initialization is conveniently done somewhere other than immediately before entering the iteration; or where updates are not required or are more readable when integrated into the more functional portions of the loop body.
The syntax of while is as follows (where reserved words and required syntactical elements are show in color):
// initializer code goes here while ( continuation_condition ) { // update code goes in loop body // C++ statements }
Note that whether they are spread out, as in while statements or
conveniently collected in one place, as in for statements, every loop
must be desigend with careful attention to the three steps of initialization,
update, and continuation or termination.
The do while statement is very similar to while, except that at least one execution of the loop body is guaranteed because the condition is executed after the loop body instead of prior to the loop body. The syntax is as follows (where reserved words and required syntactical elements are show in color):
// initializer code goes here do { // update code goes in loop body // C++ statements } while ( continuation_condition ) ;
Exercise: Find 10 examples of while and/or do-while loops in the textbook, and
re-write them all as for loops.
We conclude this chapter with some examples of code fragments that illustrate control flow structures in use. The code is C-like C++, i.e., is mostly C compatible with C++ I/O.
Code to sum the elements of an array:
sum = 0; for (size_t i = 0; i < size; ++i) { sum += A[i]; }
Note how all loop control information is in the header, which enhances the readability of the code.
This loop could be re-written as follows:
for (size_t i = 0, sum = 0; i < size; ++i, sum += A[i]) {}
with ALL actions in the header and an empty loop body! Tricks like this should be used sparingly and only when they do not detract from code readability.
Code that reads integers until EOF or a non-int is encountered:
int n; while (std::cin >> n) { // do whatever appropriate with n, which is ensured to be of type int } // after all ints are read, execution picks up here
This works neatly, because operator >> returns zero when an error occurs, which would happen if a non-int or end-of-file is encountered. The object cin could be replaced with any other object of type istream, and the type int could be replaced with any other type that cannot be confused with char. Of course, operator << must be overloaded for the type.
Code to skip clearspace in input:
do ch = in1.get(); while (!in1.fail() && ((ch == ' ') || (ch == '\t') || (ch == '\n')));
Here in1 is assumed to be an object of type istream.
Code to execute commands based on user selection from a menu of choices:
do { std::cin >> ch; // char ch = std::cin.get(); // use the get version if user is entering several commands // on the same line switch (ch) { case 'r': case 'R': read_file(); break; case 'w': case 'W': write_file(); break; case 'm': case 'M': display_menu(); break; case 'q': case 'Q': case 'x': case 'X': ch = 'x'; break; default: std::cout << "Unknown command entered - try again: "; } } while (ch != 'x');
Note how flow-through execution is used to process more than one choice with the same code. The loop executes until the user enters one of the "quit" choices, after which execution picks up after the loop. The various actions are coded as separate functions elsewhere in the program.
Code to perform mutually exclusive actions based on the value of an enumerated type:
enum color { red, yellow, red_yellow_swirl, green, brown, silver } col; // code setting value of col switch ( col ) { case red: std::cout << "Red Delicious"; break; case yellow: std::cout << "Golden Delicious"; break; case red_yellow_swirl: std::cout << "Macintosh"; break; case green: std::cout << "Granny Smith"; break; case brown: std::cout << "rotten"; break; case silver: std::cout << "iPod"; break; }
This is typical of code to overload operator << for some type, presumably type "apple". Note that the use of enumerated type adds significantly to the readability and safety of this code.
Use of "early return" or "bailout" in function implementations. This particular example implements the sequential search algorithm for an array of objects of some type T:
size_t search (const T * A, size_t size, T item) { for (size_t i = 0; i < size; ++i) if (item == A[i]) return i; return size; }
The loop just iterates through the array, comparing the elements with item that is being searched for. If item is found, the index of its location is returns, otherwise the index size is returned (which is out of the search range, hence indicating unsuccessful search).
Note that this code appears simple, but it is also subtle. The indentation is intended to make the code more readable. It could be argued that including braces enhances readability but detracts from "beauty":
size_t search (const T * A, size_t size, T item) { for (size_t i = 0; i < size; ++i) { if (item == A[i]) { return i; } } return size; }
Of course beauty is in the eye of the beholder. Which version of this code is more readable?
Here is a code fragment with an interesting history: it was a real attempt to introduce a "back door" security flaw into the Linux kernel:
if ((options == (__WCLONE|__WALL)) && (current->uid = 0)) retval = -EINVAL;
Note the "mistake"? See http://lwn.net/Articles/57135/ for discussion.