We have seen that the C++ if
statement uses a condition to selectively
execute statements. In addition to facilitating selection, most
modern programming languages also use conditions to permit statements
to be executed repeatedly.
Let's consider an example. Suppose we have 2500 words that we want
to translate into Pig Latin---about as many words as this lab
exercise. The program we wrote in Lab #5 could do this, although the
englishToPigLatin()
function only worked on one word. The
key was that the main driver repeatedly called englishToPigLatin()
for each word that the user typed in. We can
use that program to translate all 2500 words, and that's so
much easier than running the program 2500 times or writing 2500 calls
to englishToPigLatin()
.
C++ technically provides three different loops, although we'll have
four different uses for them. These are called the while
loop,
the do
loop, the for
loop, and what we call the forever loop.
This lab's exercise is to make a calculator program that has more functionality and is more user-friendly than the one we wrote in the last exercise.
Directory: lab7
calculate.cpp
is the driver.mathops.h
, mathops.cpp
, and
mathops.doc
implement a math library.menus.h
, menus.cpp
, and
menus.doc
implement a library for menus.gcc
users need a makefile; all others
should create a project and add all of the .cpp
files to
it.
Add your name, date, and purpose to the opening documentation of the code and documentation files; if you're modifying and adding to the code written by someone else, add your data as part of the file's modification history.
We will implement xn by writing a function power()
, such that a call:
power(x, n)will compute and return
x
raised to the power n
. To
simplify our task, we will assume that n
is a nonnegative
integer.
You may recall that exponentiation is available in C++ via the
function pow()
in the cmath
library. Just for this
lab, we will not use pow()
so that we can try to implement it
with loops.
As usual, we begin by using object-centered design to carefully design an exponentiation function. However, before we can describe its behavior, some simple analysis may shed light on what our function must do.
Function Analysis. When faced with a new problem, it is
often helpful to solve it by hand first. For example, to calculate
power(2,0)
, power(2,1)
, power(2,3)
and power(2,4)
by hand, we might write out expressions for these
computations:
power(2,0) = 1
power(2,1) = 2
power(2,2) = 2 * 2
power(2,3) = 2 * 2 * 2
power(2,4) = 2 * 2 * 2 * 2
The first case defines our starting point. The other cases make explicit what we already know: n tells us how many times we need to use x as factor. How do you know we have enough factors? You count them! Now if we could only count in our programs...
Function Behavior. We can describe what we want to happen as follows:
Our function should receive a base value and an exponent value from the caller of the function. It should initializeresult
to one, and then repeatedly multipleresult
by the base value, with the number of repetitions being the exponent value. Our function should then returnresult
.
Function Objects. From our behavioral description, we can identify the following objects:
Description | Type | Kind | Movement | Name |
---|---|---|---|---|
The base value | double | varying | received | base |
The exponent value | int | varying | received | exponent |
The result value | double | varying | returned | result |
From this, we can specify the task of our function as follows:
Specification:receive:base
, adouble
;exponent
, anint
.
precondition:exponent
is non-negative
return:result
, adouble
While we could worry about negative exponents, we choose not to since it would require a bit more work. We also won't test this precondition since our specification makes it quite clear that we aren't coding for this; we've given other programmers fair notice of our assumption. Our function will simply return 1, as if the exponent were 0.
Using this specification, go to mathops.h
and mathops.cpp
and replace the appropriate lines with a prototype and a
stub for power()
. Then, uncomment the call to power()
within apply()
. Finally, compile the code and fix
your errors.
The call to power()
in apply()
uses int()
to
convert the double
argument op2
to an integer.
Without this conversion, our attempt to pass a double
argument into an int
parameter would result in a compilation
error.
Function Operations. From our behavioral description, we have the following operations:
Description | Predefined? | Name | Library |
---|---|---|---|
Receive base and exponent | yes | parameter | built-in |
Initialize result to 1.0 | yes | declaration | built-in |
Set result to result *base | yes | *= | built-in |
Repeat *= exponent times | yes | for loop | built-in |
Return result | yes | return | built-in |
Function Algorithm. We can organize these operations into the following algorithm:
base
and exponent
from caller.result
to 1.0.count
from 1 to exponent
:
result
*= base
.result
.The C++ for
loop is designed to facilitate the counting
behavior required by step 2. The pattern for the counting
for
loop is:
for (whereType
Var
=Start
;Var
<=Stop
;Var
++)Statement
Type
is a C++ numeric type, Var
is the
loop control variable used to do the counting, Start
is
the first value, and Stop
is the last value.
For example, if we wanted to print x
20 times, here's the
algorithm and the code:
|
for (int i = 1; i <= 20; i++) cout << x; |
power()
function. Read the
algorithm carefully. There are several variables that you have to
create and use in writing your loop, so read the algorithm
carefully. Remember that you aren't supposed to invent new things
with your code; just translate the algorithm above.
When you've written the code, compile and test your program. Test exponentiation on several values. Make sure you test 0 as an exponent. Try values larger than 2 and 3 as both bases and exponents.
The pattern for a C++ for
loop is actually more general:
for (whereInitializationExpr
;Condition
;StepExpr
)Statement
InitializationExpr
is any initialization expression,
Condition
is any boolean expression, and StepExpr
is an arbitrary expression.
If for some reason we wanted to count downwards and output the multiples of 12 from 1200 to 12, then we'd have this algorithm and code:
|
for (int i = 100; i >= 1; i--) cout << 12 * i << endl; |
The Condition
controls the loop. As long as it is true
when it is tested, then the loop Statement
is executed.
C++ for
loops are controlled by conditions, just as if
statements are controlled by conditions. As we shall see, each
of the other C++ loop statements are also controlled by conditions.
A loop is categorized by when it evaluates its condition:
The for
loop is a pretest loop, because it evaluates its
condition before the loop's statement is executed. You can prove it
to yourself: power()
should return 1 if the exponent is 0
(which you know since you tested this above). The only way
this happens is if the loop in power()
does not execute its
statement; if the statement did execute, then the value
returned wouldn't be 1.
The for
loop 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. Many problems need
other types of repetition.
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.
while
loop provides a general pretest loop.do
loop provides a general posttest loop.Let's try out these loops to handle our menu input.
getMenuChoice()
is defined in menus.cpp
.
char getMenuChoice(const string MENU) { cout << MENU; char choice; cin >> choice; return choice; }What happens if the user types in a bad value? Try it out.
We can be more user-friendly by handling the input errors in this function.
One way to handle such errors is to repeatedly display the menu and input the user's choice, so long as they continue to enter invalid menu choices. This isn't something we can predict since we have no idea how many times the user will enter in bad data.
The general-purpose loops give us a way to handle user errors, but we
must decide which one to use. We can begin by writing a partial
algorithm for this problem using a generic Loop
statement, in which we
don't worry (for the moment) how control will leave the loop:
MENU
.choice
.choice
.We've looked at how conditions are used in if
and for
statements. The other loops work quite similar to the for
loop. There are two types of conditions we have to work with.
For our problem, we want the repetition
choice
is an invalid menu
choice, andchoice
is a valid menu choice.Since choice
is not known until after Step (b), it
seems logical to see if we're done after that step, which we
can describe using our termination condition as follows:
MENU
.choice
.choice
is a valid menu choice, exit the loop.choice
.In C++, the post-test loop is called the do
loop, and its pattern is as
follows:
do {When execution reaches such a loop, the following actions occur:Statements
} while (Condition
);
Statements
execute.Condition
is evaluated.Condition
is true, control returns to step 1;
otherwise, control proceeds to the next statement after the loop.do
loop uses a continuation condition.
Since its Condition
is not evaluated until after the first
execution of Statements
, the 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 is a bit tricky. One way to
handle it is to require that the valid menu choices be consecutive
letters of the alphabet (e.g., 'a'
through 'e'
). If
we then pass the first and last valid choices to getMenuChoice()
as arguments:
char operation = getMenuChoice(MENU, 'a', 'e');and add parameters to the function prototype and definition to store these arguments:
char getMenuChoice(const string MENU, char firstChoice, char lastChoice);
Add the new arguments to the function call in the driver. Add the new parameters to the prototype and the function itself. You can recompile the code to make sure the compiler is happy with your changes, but the program won't run any differently yet.
Since 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 > lastChoiceWe are then ready to add a
do
loop to the function to implement
our algorithm.
Code it up. Here are the pieces we need for the do
loop:
Statements
are the output of MENU
and the
input of choice
.Condition
is in the previous paragraph.choice
outside the loop so that it
can be returned outside the loop.Compile and test your code.
Another potential source of error occurs when our program reads
values for 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 the good()
method of cin
.
But suppose we wished to give the user another chance to enter valid
values, instead of just terminating the program (which seems terribly
drastic)? At first glance, it looks like 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 is inappropriate because of two subtleties about
cin
input (actually, istream
input in general).
When the >> operator is expecting a real value, but gets a non-real
value, two things happen, each of which causes a difficulty:
cin
is set so that its method
good()
will return false. No input operations can be
performed with cin
so long as cin.good()
returns
false.The first problem can be fixed with cin.clear()
, which resets
the status of cin
so that cin.good()
returns true
again.
The second problem takes some more work. We don't want the loop to
continue reading the same bad value over and over and over again. We
need some way to skip over the bad input. This can be accomplished
using the ignore()
method:
cin.ignore(Characters inNumChars
,UntilChar
);
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
call to solve this difficulty:
cin.ignore(120, '\n');
All of this complicates our choice of which loop to use, because we now have four steps to perform:
op1
and op2
. [One or more times.]cin.clear();
[Only if necessary, zero or more times.]cin,ignore(120, '\n');
[Only if necessary, zero or
more times.]do
loop isn't going to work.
One traditional solution is modify our algorithm to use a pre-test loop in the following way:
op1
and op2
.cin.good()
is false:
cin.clear();
cin.ignore(120, '\n');
op1
and op2
.The C++ pretest loop is called the while
loop:
while (As usual,Condition
)Statement
Condition
is any C++ Boolean expression, and
Statement
can be either a single or compound C++
statement. Statement
is often referred to as the body of the loop.
This works just 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
.
Add a while
loop to encode our modified algorithm. The
condition !cin.good()
can be used to control the while
loop. Since the while
loop must repeat multiple
statements, its Statement
must be a compound statement or
bad things will result.
Compile and thoroughly test what you have written. Continue when what you have written makes the entry of numeric values fool-proof.
The algorithm that our program is using is basically as follows:
operation
,
guaranteed to be a valid menu operation.op1
and op2
.result
by applying operation
to
op1
and op2
.result
.To add the loop, we modify our algorithm:
operation
,
guaranteed to be a valid menu operation.op1
and op2
.cin
.op1
and op2
.result
by applying operation
to
op1
and op2
.result
.One way to have the user indicate that they want to quit is to view
quitting as an operation, and provide an additional menu choice
(i.e., 'f'
) by which the user can indicate that they want to
quit. The condition (operation == 'f')
evaluates to 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 known; 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. It must be evaluated in the
middle of the loop.
operation
,
guaranteed to be a valid menu operation.operation
is quit, exit the loop.op1
and op2
.cin
.op1
and op2
.result
by applying operation
to
op1
and op2
.result
.In C++, the unrestricted loop is a simplification of the for
loop that we call the forever loop, that has the following pattern:
for (;;) {By leaving out the three expressions that normally control aStatementList1
if (Condition
) break;StatementList2
}
for
loop, it becomes an infinite loop. Of course, we do not want an infinite number
of repetitions, but instead 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.
Modify your source program to incorporate this approach, and then translate and test the newly added statements.
This loop can also be used for the "valid numeric input" loop that we wrote before. Give it a try.
Turn in your code and sample runs of your program to demonstrate that it does everything we added to it this week. We added a lot of things, including some error handling, so make sure you test it fully.
for
loop, do
loop, forever loop, general-purpose
loop, generic loop, infinite
loop, loop body, one-trip behavior, posttest loop, pretestloop, repeated, selective execution, termination condition, unrestricted loop, zero-trip behavior