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 ifs 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)Statement1else if (Condition2)Statement2... else if (ConditionN)StatementNelseStatementN+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 ifs.
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 elsein 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 elsein anyifstatement 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;op1andop2, twodoublevalues.
return: thedoubleresult of applyingoperationtoop1andop2.
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) {CaseList1StatementList1CaseList2StatementList2...CaseListNStatementListNdefault: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 ifshould always have a fail-safeelseclause, you should always have adefaultcase in aswitchstatement, 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 returns 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 switchstatement 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.