Lab 7: More Control Structures


In Lab 4, we considered one of C++'s selection structures — the if statement — and two of its repetition structures the for and the while statements. This lab exercise introduces two more control structures:

Part 1: Selection Structures

Up to now, the only selection structure we have been using is the if statement. It is one of C++'s most versatile statements in that it can be used to implement both two-alternative and multialternative selections. In the first part of this lab exercise we look at another selection structure — the switch statement — that is more efficient in some cases, but is limited in its applicability.

Getting Started

In Lab 4, we developed a simple 5-function calculator program that allowed the user to select from a menu of operations:

In this lab we will start with a file calculate.cpp that differs from that in Lab 4 in that the menu options are labeled a, b, c, d, e instead of +, -, *, /, ^ and two functions are used:
getMenuChoice(): to get the operation selected by the user
apply(): to apply the operation to the operands

These may seem like rather minor changes and the menu relabeling even an undesirable one because it is much easier to remember to enter
      + for addition instead of a,
      - for subtraction instead of b,
      * for multiplication instead of c, and so on.
But we will see the reason making this change in the second part of this lab exercise.

Here is a listing of main() in calculate.cpp:

int main()
{
  //*** ENTER YOUR OPENING OUTPUT STATEMENT(S) HERE

  const string MENU = "\nPlease enter:\n"
                      "\ta - to perform addition;\n"
                      "\tb - to perform subtraction;\n"
                      "\tc - to perform multiplication;\n"
                      "\td - to perform division;\n"
                      "\te - to perform exponentiation;\n"
    "--> ";

  char operation = getMenuChoice(MENU);

  double op1, op2, result;
  cout << "Now enter your operands: ";
  cin >> op1 >> op2;
  assert( cin.good() );

  result = apply(operation, op1, op2);
    
  cout << "The result is " << result << endl;
}
You should download/copy calculate.cpp into whatever directory/folder/project you are using for this lab exercise. In the program in Lab #4 we used an if ... else if ... else statement to process the user's menu selection. We now look an alternative way to implement this multialternative selection structure.

The switch Statement

The multi-branch if suffers from one drawback:

Now consider the following general multi-branch if statement:

if (Condition1)
  Statement1
else if (Condition2)
  Statement2
...
else if (Conditionn)
  Statementn
else
  Statementn+1

For Statementi to be selected, the i conditions Condition1, Condition2, ..., Conditioni-1, Conditioni must be evaluated. This can be just what is needed when, for example, it is necessary to know that all of the tests preceding the ith one did, in fact, fail.

On the other hand, the evaluation of each condition takes time; statements that occur later in the multi-branch if statement take longer to execute than statements that occur earlier. In some situations, synchronization is required; the time required to reach any of the Statementi must be the same for all i.

We look now at one way this can be avoided — by using a switch statement, which has the form

switch ( SelectorExpression )
{
  CaseList1
    StatementList1
  CaseList2
    StatementList2
     ...
  CaseListn
    StatementListn
  default:
    StatementListn+1
}

where

Pay careful attention to the terminology here.

A switch statement is executed as follows:

  1. The SelectorExpression is evaluated.
  2. If the value of the expression is present in CaseListi, then execution begins in StatementListi and continues until is encountered.
  3. If the value of SelectorExpression is not present in any CaseListi, then the (optional) default StatementListn+1 is executed.
  4. No Constant may appear in more than one CaseListi.

Helpful hint: Just as a multi-branch if should always have a fail-safe else clause, you should always have a default case in a switch statement, even if it simply prints an error message.

Perhaps the trickiest thing about a switch statement is the purpose of the cases. They are merely labels for the compiler to tell it where to start executing code. Consider this code, where i is an integer:

switch (i)
{
  case 0:
    cout << "Zero ";
  case 1:
  case 2:
    cout << "One Two ";
  default:
    cout << "Other";
}

Then, if i is 0, this outputs

Zero One Two Other
If i is 1 or 2, it outputs
One Two Other

If i is any other value, it outputs

Other

As this example illustrates, the various cases specify to the compiler where execution of the code is to begin, but not when to stop — rather, execution "falls through" to the end of the switch. This is useful in a few situations, but it isn't what we want here. When we use our 5-function calculator and select option a to add two numbers, we don't want to also subtract them, multiply them, divide them, and raise one to the other power.

To stop executing the code at the end of a statement list in one of the cases, we can put a break statement, a return statement, or a call to exit() at the end of the list; for example,

switch (i)
{
  case 0:
    cout << "Zero ";
    break;
  case 1:
  case 2:
    cout << "One Two ";
    break;
  default:
    cout << "Other";
}

Now, if i is 0, this outputs

Zero 
and if i is 1 or 2, it outputs
One Two 
and if i has any other value, the output will be
Other

We have used the return statement to return a value from a function; and we have used the exit() function to stop a program in case of an error. The break statement in the preceding example is even simpler:

break;
The following are the key things to remember about using the switch statement:

Things to Remember:

  • The statement list of every case list in a switch statement should end with a break, a return, or an exit().
  • Each case must be a constant.
  • The SelectorExpression and the case values must all be integer-compatible.
  • Finding a matching case is done only through equality tests.

The integer-compatible data types in C++ include int, (of course), short, long, unsigned, char, and bool.
It does not include
double or string.

 
As a guide: If you find yourself using an algorithm of the form on the left of chart below where Expression is integer-compatible, you can use the switch in the middle, which is equivalent to the multi-branch if on the right:

 Set Variable to Expression.
 If (Variable is equal to Value1)
  StatementList1
 Else if (Variable is equal to Value2) 
   StatementList2
  ...
 Else if (Variable is equal to Valuen) 
   StatementListn
 Else
   StatementListn+1
 End if
 switch (Expression) 
 {
   case Value1:
     StatementList1
     break;
   case Value2:
     StatementList2
     break;
    ...
   case Valuen:
     StatementListn
     break;
   default:
     StatementListn+1 
 }
 Variable = Expression;
 if (Variable == Value1)
 {
   StatementList1
 }
 else if (Variable == Value2) 
 {
   StatementList2
 }
 ...
 else if (Variable == Valuen)
 {
   StatementListn
 }
 else
 {
   StatementListn+1
 }

A switch statement is more efficient than the multi-branch if statement because a switch statement can select any StatementListi in the time it takes to evaluate one condition. The switch statement thus eliminates the non-uniform execution time of statements controlled by a multi-branch if statement. But the restrictions on the switch statement do limit its use.


Part 2: Repetition Structures — do and forever Loops

The two repetition structures that we used in Lab #4 were a for loop and a while loop. A for loop is designed to count through a range of values and we used it to implement the exponentiation operation. We used a while loop to allow the user to repeatedly select a menu option, stopping when a "special" operation was selected. In this lab exercise, we will look at two other kinds of loops: a do loop and a forever loop.

Characterizing Loops

A loop is categorized by when it evaluates its condition:

  1. A pretest loop evaluates its condition before its statements.
  2. A posttest loop evaluates its condition after its statements.
  3. An unrestricted loop evaluates its condition wherever we specify.

The for loop is a pretest loop, because it evaluates its condition before the loop's statement is executed. It is designed primarily for problems that involve counting through ranges of numbers or problems in which the number of repetitions can be determined in advance. For many problems, however, we don't know how many repetitions will be needed.

The other three C++ loops differ from the for loop in that they are general-purpose loops, designed for problems where the number of repetitions is not known in advance.

  1. The C++ while loop provides a general pretest loop.
  2. The C++ do loop provides a general posttest loop.
  3. The C++ forever loop provides a general unrestricted loop.

Let's see which of these loops to use to handle menu input.

Getting a Valid Menu Choice

getMenuChoice() is defined in calculate.cpp as follows:

char getMenuChoice(const string MENU)
{
  cout << MENU;
  char choice;
  cin >> choice;
  return choice;
}

We can make our program more user-friendly by handling input errors in this function. One way is to reject bad values entered by the user and to keep on displaying the menu until the user enters a valid menu item. How many repetitions will be needed before this happens isn't something we can predict because we have no idea how many times the user will input bad data.

The general-purpose loops give us a way to handle such user errors, but we must decide which one to use. We can begin by using a generic loop in which we don't worry (for now) how control will leave the loop:

  1. Loop:
    1. Display MENU.
    2. Read choice.
    End loop.
  2. Return choice.

We have seen how conditions are used in if and for statements to alter the default sequential flow of execution. Similarly, for general purpose loops, there are two types of conditions we can use:

The kind of loop we use will determine what type of condition we need. When we change from one loop to another, we may have to change the type of condition.

For our problem, we want the repetition to

Since choice is not known until after the user enters it, we can't check its validity until after Step (b) of the preceding generic loop

  1. Loop:
    1. Display MENU.
    2. Read choice.
    3. If choice is a valid menu choice, exit the loop.
    End loop.
  2. Return choice.
In this algorithm, because the controlling condition is evaluated at the bottom of the loop, a post-test loop is the appropriate loop to choose. And in C++, a do loop is such a post-test loop; its pattern is as follows:
do
{
  Statements
}
while ( Condition );
When execution reaches such a loop, the following actions occur:
  1. Statements are executed.
  2. Condition is evaluated.
  3. If Condition is true, control returns to step 1; otherwise, control proceeds to the next statement after the loop.
Because repetition continues as long as the condition is true, a do loop uses a continuation condition.

Also, note that its Condition is not evaluated until after the first execution of Statements. This means that a do loop guarantees that Statements will be executed one or more times. For this reason, the do loop is said to exhibit one-trip behavior — we must make at least one trip through Statements.

The notion of a "valid" menu choice can be a bit tricky. One way to make this straightforward is to use consecutive numbers or letters of the alphabet for our menu selections; — e.g., 'a' through 'e' as in the menu of operations in this lab.

How does this help? We can pass the first and last valid choices to getMenuChoice() as arguments,

char operation = getMenuChoice(MENU, 'a', 'e');
and add parameters to the getMenuChoice()'s prototype and definition to store these arguments:
char getMenuChoice(const string MENU, char firstChoice, char lastChoice);
Exer. 7.5. Add the new arguments to the function call in the driver program. Add the new parameters to the prototype and the definition of getMenuChoice(). Recompile the program to check for errors. However, the program won't run any differently yet.

Because firstChoice and lastChoice will define the range of valid choices, we can express our loop's continuation condition in terms of those values: choice is invalid if and only if

choice < firstChoice || choice > lastChoice
We are now ready to add a do loop to the getMenuChoice() function to implement our algorithm.
Exer. 7.6. Add a do loop to the definition of the getMenuChoice() function.

Compile and test your code.


Getting Valid Numeric Input

Another potential source of error occurs when our program reads values for the operands op1 and op2 from the keyboard — the user might enter some non-numeric value. Our current version checks for this using the assert() mechanism and cin's built-in member function good().

But suppose we wish to give the user another chance to enter valid values instead of terminating execution? At first glance, it looks as though we could use the same approach as before, by replacing the assert() with a posttest loop that repeats the steps so long as good() returns false:

do
{
  // prompt for op1 and op2
  // input op1 and op2
}
while ( !cin.good() );
However, this approach doesn't work well because of two subtleties about cin input (actually, istream input in general). When the >> operator is expecting a value of a certain type but gets a non-compatible value, two things happen, each of which causes a difficulty:

  1. When input via cin fails, its internal status is set so that its built-in function member good() will return false. No input operations can be performed with cin so long as cin.good() returns false.
  2. After failed input, the bad input value stays (unread) in the input stream.

The first problem can be fixed with cin.clear(), which resets the status of cin so that cin.good() returns true again.

But the second problem is more difficult to fix. We don't want the loop to continue reading the same bad value over and over and over again, so we need some way to skip over the bad input. To accomplish this, we can use cin's ignore() member function:

cin.ignore( NumChars, UntilChar );
Characters in cin will be skipped until NumChars have been skipped or until the character UntilChar is encountered, whichever comes first. Since lines are usually not more than 120 characters in length, and the end of a line of input is marked by the newline character ('\n'), we can use the following to solve this difficulty:
cin.ignore(120, '\n');

All of this, however, complicates our choice of which loop to use, because we now have four steps to perform:

  1. Display a prompt for two real values. [One or more times.]
  2. Input op1 and op2. [One or more times.]
  3. cin.clear(); [Only if necessary — zero or more times.]
  4. cin.ignore(120, '\n'); [Only if necessary — zero or more times.]
It looks like our loop-exit test should be placed in the middle — between steps (b) and (c). A do loop isn't going to work.

One traditional solution is modify our algorithm to use a pre-test loop in the following way:

  1. Display a prompt for the two operands.
  2. Input op1 and op2.
  3. Loop while cin.good() is false:
    1. cin.clear();
    2. cin.ignore(120, '\n');
    3. Display a prompt for two real values.
    4. Input op1 and op2.
    End loop.

The C++ pretest loop is the while loop,

while ( Condition )
  Statement
a pretest loop that we have used before in Lab #4. It works like a do loop except that Condition is evaluated before the Statement is evaluated. If Condition is false right away, Statement is never executed. For this reason, a while loop is said to exhibit zero-trip behavior — we might make zero trips through Statement.
Exer. 7.3. Replace the assert(cin.good()); statement in main() with a while loop to to implement the while loop in the preceding algorithm. The body of the loop consists of four statements to implement the four statements in the loop of the algorithm — the first two to fix up cin and the last two to output a prompt for the real operands and then read them.

Compile and test your code.


One Execution, Multiple Calculations

The algorithm that our program is using is basically as follows:

  1. Display opening output.
  2. Define the menu of operations.
  3. Call getMenuChoice() to get a value for operation, guaranteed to be a valid menu operation.
  4. Display a prompt for two operands.
  5. Input op1 and op2.
  6. Loop while a bad operand (not a real value) was entered:
    1. Clear the status of cin.
    2. Skip the invalid input.
    3. Display a prompt for two operands.
    4. Input op1 and op2.
    End loop.
  7. Compute result by calling apply() to apply operation to op1 andop2.
  8. Display result.
Using this algorithm, we have to re-run our program for each calculation, which is inconvenient for the user. A more convenient calculator would wrap some of the statements in a loop, so that the user could perform multiple calculations without having to re-run the program.

To accomplish this, we enclose everything after steps 1 and 2 within a loop as follows:

  1. Display opening output.
  2. Define the menu of operations.
  3. Loop:
    1. Call getMenuChoice() to get a value for operation, guaranteed to be a valid menu operation.
    2. Display a prompt for two operands.
    3. Input op1 and op2.
    4. Loop while a bad operand (not a real value) was entered:
      1. Clear the status of cin.
      2. Skip the invalid input.
      3. Display a prompt for two operands.
      4. Input op1 and op2.
      End loop.
    5. Compute result by calling apply() to apply operation to op1 andop2.
    6. Display result.
    End loop.
But, what kind of loop should we use? To answer this, we have to determine where to check the loop's termination condition, which we might express as "the user wants to quit."

In a menu-driven program such as this, a common approach is to add an option to the menu that the user can select to quit — typically, the last item on the menu. So we will provide an additional menu choice 'f' for quitting. The condition (operation == 'f') will be true if the user wishes to quit.

So when do we check this condition? The ideal place is to evaluate it as soon as it can be checked; in this case this would be immediately following the input of operation. We thus have a situation where the loop's condition should not be evaluated at the loop's beginning nor at its end but rather, somewhere "in the middle" of the loop.

  1. Display opening output.
  2. Define the menu of operations.
  3. Loop:
    1. Call getMenuChoice() to get a value for operation, guaranteed to be a valid menu operation.
    2. If the "quit" operation has been selected, use break to exit from the loop.
    3. Display a prompt for two operands.
    4. Input op1 and op2.
    5. Loop while a bad operand (not a real value) was entered:
      1. Clear the status of cin.
      2. Skip the invalid input.
      3. Display a prompt for two operands.
      4. Input op1 and op2.
      End loop.
    6. Compute result by calling apply() to apply operation to op1 andop2.
    7. Display result.
    End loop.

In C++, one way to implement an unrestricted loop is with a special form of the for loop that we call a forever loop and that has the following pattern:

for (;;)           // or as some prefer:  while(true)
{
  StatementList1
  if ( Condition ) break;
  StatementList2
}
By leaving out the three expressions that normally control a for loop, it becomes an infinite loop. Of course, we do not want an infinite number of repetitions; rather, we want execution to leave the loop when a termination condition becomes true. As shown in the pattern above, this can be accomplished by placing an if statement that uses a termination condition to control a break statement within the body of the loop.

When a break statement is executed, execution is immediately transferred out of the forever loop to the first statement following the loop. By only selecting the break statement when Condition is true, a forever loop's repetition can be controlled in a manner similar to the other general-purpose loops.

Exer. 7.8. In main(): (i) first add another item to the menu of operations:
       f - to quit
and modify the call to getMenuChoice() appropriately.

(ii) then add a forever loop to implement the forever loop in the preceding algorithm.

Compile and test your code.

Note: A forever loop can also be used for the "valid numeric input" loop that we wrote before. Make that change in the program, if you like.

Hand In:

Your code and a sample run of your program to demonstrate that everything works — that is, an execution:


Lab Home Page


Report errors to Larry Nyhoff (nyhl@cs.calvin.edu)