One of the easiest ways to make a program user-friendly is to make it menu driven. That is, rather than prompting the user in some vague sort of way, we present them with a menu of the choices available to them. Then all the user has to do is look at the menu and choose one of the choices. Since the menu always tells the user what their options are, the user needs no special knowledge, making such a program easy to use.
For example, a simple 4-function calculator program might prompt the user by providing the following menu:
Please enter: + to add two numbers. - to subtract two numbers. * to multiple two numbers. / to divide two numbers.Thanks to the menu, a user knows exactly what to enter.
This lab's exercise is to complete such a program, and at the same time learn about some of the C++ control structures for selection.
Let's first take a look at some old code. In the previous lab, we looked at this function:
string pluralize (string singularNoun) { int lastCharIndex = singularNoun.size() - 1; char lastChar = singularNoun[lastCharIndex]; if ((lastChar == 's') || (lastChar == 'x')) return singularNoun + "es"; else if (lastChar == 'y') { string base = singularNoun.substr (0, lastCharIndex); return base + "ies"; } else return singularNoun + "s"; }
This function used a multi-branch if
to decide which
rule to apply in making the noun plural.
if
StatementThe general pattern for an if
statement looks like this:
if (Condition
)Statement1
[elseStatement2
]
Either Statement1
or Statement2
, but not both, are executed based on the value of
Condition
. If Condition
is true
,
then Statement1
is executed; otherwise (i.e.,
Condition
is false
), Statement2
is executed.
The square brackets [...]
in the pattern indicates that the
else
clause is optional. Sometimes a simple if
, without an else
, is all we need.
if
StatementWe can also chain several if
s together into one statement.
Now generally it's not advisable to put an if
statement in as
Statement1
of another if
. These can get
confusing. However, using an if
statement for Statement2
is perfectly acceptable---encouraged, even. In
general, a multi-branch if
looks like this:
if (Condition1
)Statement1
else if (Condition2
)Statement2
... else if (ConditionN
)StatementN
elseStatementN+1
Consider the pluralizer above. There are four possibilities for the
singular noun: it ends in an 's', it ends in an 'x', it ends in a
'y', or it ends in something else. This chain of rules/conditions
leads to a chain of if
s.
Exactly one of the StatementI
will be
executed. It will be the first StatementI
where ConditionI
is true
. All of the
previous conditions must be false. If all conditions fail, then the
fail-safe StatementN+1
is executed. This is exactly what we want and need for the pluralizer; you'll also need
it for the code in this lab.
Helpful hint: Include a fail-safe else
in a multibranchif
, even if it's just a debugging statement.
The debugging statement can be as simple as a statement that prints an error message like "This statement should not print". This will make things much easier for you to debug your program when (not if) something goes wrong.
Also, there is no condition after the last else
.
Helpful hint: The last else
in anyif
statement should not have a condition test.
When any else
clause is triggered, we know that the
previous test failed, so there's no need to make a further test. For
example, we could write:
if ((lastChar == 's') || (lastChar == 'x')) return singularNoun + "es"; else if ((lastChar != 's') && (lastChar != 'x') && (lastChar == 'y')) { string base = singularNoun.substr (0, lastCharIndex); return base + "ies"; } else if ((lastChar != 's') && (lastChar != 'x') && (lastChar != 'y')) return singularNoun + "s";
But the extra tests in this version are completely unnecessary.
When any of the !=
tests are evaluated in this new
multi-branch if
, they will always be true because they
only made after the corresponding ==
tests were
discovered false. This is why the original version of pluralize()
does not have these unnecessary !=
tests.
The patterns above for the if
and multi-branch if
statements were carefully crafted. In particular, we used the singular "Statement
", not "Statements
". We
can put only one statement in those places in the patterns.
What if we need more than one statement? Well, that's exactly what we needed with the second rule for pluralizing a word which involves two statements. C++ allows us to wrap several statements in curly braces and treat them as one:
{Statement1
Statement2
...StatementN
}
So that's why there are those curly braces around the two statements
for the second rule. If we dropped them, then the compiler would get
confused over the last else
since there isn't a corresponding
if
close enough for it.
Directory: lab6
calculate.cpp
implements the driver.mathops.h
, mathops.cpp
, and
mathops.doc
implement a math library.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.
Take a few moments to study calculate.cpp
, particularly the
code in the main()
routine. Make sure you understand the
purpose of each statement in calculate.cpp
before going any
further in this lab.
Ignoring its error-checking code, main()
should behave like
so:
Our program should display on the screen a greeting, followed by a menu of permissible operations. It should then read a user-specified operation from the keyboard. It should then prompt the user for the two operands for that operation, and then read those operands from the keyboard. It should then compute the result of applying the user-specified operation to the two operands. It should conclude by displaying that result on the screen, with appropriate labeling.
All of this behavior is coded up in main()
expect for the
sentence written in boldface. Since there is no predefined C++
capability to directly perform that operation, we will write a
function apply()
to do that action. This function is
(arguably) useful beyond just this program, so we'll put it off in a
library.
As usual, we use object-centered design to develop this function.
Behavior. Our function must apply operation
to
op1
and op2
and return the result. We can
describe the needed behavior as follows:
Our function should receive from its caller an operation, and two operands. If the operation is'+'
, our function should return the sum of the two operands. If the operation is'-'
, our function should return their difference. If the operation is'*'
, our function should return their product of the two operands. If the operation is'/'
, our function should return their quotient.
Objects. From this behavioral description, we can identify the following objects:
Description | Type | Kind | Movement | Name |
---|---|---|---|---|
The operation | char | varying | received | operation |
One of the operands | double | varying | received | op1 |
The other operand | double | varying | received | op2 |
The sum of the operands | double | varying | out | op1 + op2 |
The difference of the operands | double | varying | out | op1 - op2 |
The product of the operands | double | varying | out | op1 * op2 |
The quotient of the operands | double | varying | out | op1 / op2 |
From this list, we can specify the behavior of our function as follows:
Specification:receive:operation
, achar
;op1
andop2
, twodouble
values.
return: thedouble
result of applyingoperation
toop1
andop2
.
Please, please, please save yourself much pain and watch the name of the first parameter:
Helpful hint: Do not name the first parameter operator
. It's a keyword, and the compiler may not give you the best error messages if you do use it as a variable.
Using the specification (and heeding the hint), add a prototype for a
function named apply()
in mathops.h
and a stub in
mathops.cpp
.
Function Operations From our behavioral description, we have these operations:
Description | Predefined? | Name | Library |
---|---|---|---|
Receive operation (a char ) | yes | function call mechanism | built-in |
Receive op1 (a double ) | yes | function call mechanism | built-in |
Receive op2 (a double ) | yes | function call mechanism | built-in |
Return the... | yes | return | built-in |
...sum of op1 and op2 | yes | + | built-in |
...difference of op1 and op2 | yes | - | built-in |
...product of op1 and op2 | yes | * | built-in |
...quotient of op1 and op2 | yes | / | built-in |
Do exactly one of math operations | yes | if statement | built-in |
C++ provides facilities for performing each of these operations as
noted in this chart. The last operation is different from the
others, in that it requires selective behavior. We can use the
if
statement.
Function Algorithm. We can organize these operations into the following algorithm:
operation
, op1
and op2
.operation
is '+':
op1
+ op2
.operation
is '-':
op1
- op2
.operation
is '*':
op1
* op2
.operation
is '/':
op1
/ op2
.From this algorithm it is clear that a multi-branch if
will
do the trick. The last case is technically unnecessary since our
program checks the operation, but as noted above you shouldn't rely
on that. Print out an error message here so that it's very
clear that something went wrong. If you want, you can even quit the
program (with exit(1);
) instead of returning 0, which is
merely an arbitrary value in this case.
Add a multi-branch if
to your apply()
stub to encode
this step.
Testing and Debugging.
When you are done, compile everything and test your program. Fix it up until it works correctly. Be sure to test each operator at least twice.
switch
StatementThe multi-branch if
suffers from one drawback:
(operation == '+')
.(operation == '+')
and (operation == '-')
.(operation == '+')
, (operation == '-')
, and (operation == '*')
.In general, selecting Statementi
using a multi-branch
if
statement requires the evaluation of i conditions.
This can be good because we can assume that the failed test did, in
fact, fail. When testing for ranges of values, this is incredibly
useful (and even necessary).
On the other hand, the evaluation of each condition consumes time,
statements that occur later in the multi-branch if
statement
take longer to execute than do statements that occur earlier. It
would be great if we could avoid this if at all possible. One
possibility is using a switch
statement.
A switch statement
looks like this:
switch (ConstantExpression
) {CaseList1
StatementList1
CaseList2
StatementList2
...CaseListN
StatementListN
default:StatementListN+1
}
ConstantExpression
is any C++ expression that evaluates to
a integer-compatible constant. Each StatementListi
is a sequence of valid C++ statements. Each
CaseListi
is one or more cases of the form:
case Constant
:
Constant
is an integer-compatible constant.
That's a fair bit of code. Watch the careful use of terminology here,
particularly "integer-compatible constant". Let's first figure out
what a switch
statement does:
ConstantExpression
is evaluated.CaseListI
, then execution begins in StatementListI
and proceeds, until a break
statement, a return
statement, or the end of the switch
statement is encountered.ConstantExpression
is not present in
any CaseListI
, then the (optional) default
StatementListN+1
is executed.A given Constant
can appear in only one CaseListi
.
Helpful hint: Just as a multi-branch if
should always have a fail-safeelse
clause, you should always have adefault
case in aswitch
statement, even if it just prints an error message.
Perhaps the trickiest thing about a switch
statement is the
purpose of the cases. The cases of the switch statement are merely
labels for the compiler to tell it where to start
executing code. Consider this code:
switch (i) { case 0: cout << "Hi "; case 1: cout << "there, "; default: cout << "people!"; }
Then, if i
is 0, this prints
Hi there, people!
The cases tell the compiler where to start executing the code,
not when to stop. While this is useful in some situations, it
won't be for us. When I make a noun plural, I want to apply only
one of the rules. When I add two numbers together, I don't
also want to multiply them. To stop executing the code at the end of
a statement list, we can the statement list with a break
statement, a return
statement, or a call to exit()
.
We've used return
s for returning values from a function.
We've also use the exit()
function for stopping the program
in case of an error. The break
statement is even simpler:
break;
Helpful hint: The statement list of every case list in a switch
statement should end with abreak
, areturn
, or anexit()
.
So when would we use a switch
statement? Whenever we can.
Unfortunately, there are a few restrictions:
case
must be a constant.ConstantExpression
and the case values must all
be integer-compatible.case
is done only through
equality tests.The integer-compatible data types in C++ include int
(of
course), char
, and bool
. It does not include
double
or string
. That's very important, so maybe
you should read it again, this time out loud: "integer-compatible" does not include double
or string
.
Consider the chart below. If we write the algorithm on the left, we
can use the switch
in the middle which is equivalent to the
multi-branch if
on the right:
Set |
switch ( |
|
A switch
statement is more efficient than the multi-branch
if
statement because a switch
statement can select
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 since the switch
statement has so
many restrictions on it, we use it only occasionally.
We have a slight wrinkle with pluralize()
because it's first
test actually tests two things. However, both tests are
equality tests with constants and one variable. We can use two
cases for one statement list in a switch
statement.
So we could rewrite pluralize()
like so:
string pluralize (string singularNoun) { int lastCharIndex = singularNoun.size() - 1; char lastChar = singularNoun[lastCharIndex]; switch (lastChar) { case 's': case 'x': return singularNoun + "es"; case 'y': string base = singularNoun.substr (0, lastCharIndex); return base + "ies"; default: return singularNoun + "s"; } }
We're careful in this code to end each statement list with a return
. Each case
has an integer-compatible (i.e., char
) constant.
Try it out for yourself,
if
version of apply()
so that
you have two copies of it in the same file.apply2()
.if
in apply()
with an
equivalent switch
statement. (Leave apply2()
alone.)When everything is working right, save your work and print a hard copy of your code.
Turn in your code and sample runs that proves your calculator works for all operations.