| |
Main Menu | Next Section |
| Section III: C++ Code | Section I: User Interface Analysis - Giving Olympia Choices | Section II: Functional Decomposition of 'main' | Section IV : Putting It All Together |
Section III. C++ Code
Let's try our hand at writing the C++ code for the algorithm for 'ProcessUserChoices'.
We can do this even though we have not examined closely any of
the functions that this algorithm uses. While designing
and writing 'ProcessUserChoices', you should not care or concern
yourself at all with HOW any other functions work. Simply assume
that they do work as they are supposed to - in other words, assume
that when the time comes, you, or whoever is assigned the task,
can write them correctly.
This mind set - that I don't care about anything other than what
I am working on right now - is often hard for beginners. They
often worry about the details of some other part of the program
instead of focusing on the part they are presently working on.
It may be true that the code presently under consideration will
not work if some other part is coded incorrectly and it may be
true that that other part involves some programming challenge
but worrying about that other part only makes the present code
more difficult.
A. Defining "ProcessUserChoices" and the Built-in "bool" Type
The declaration for 'ProcessUserChoice' is simple, given that there
are no receives and returns:
Writing the definition for this function is where the challenge is. The first issue involves the boolean variable. The most recent standard for C++ includes the 'type' bool to handle boolean variables. Such variables can only hold the values 'false' and 'true' which are also built into the system. To declare a variable called 'done' of type 'bool', one simply writes:
Once this line is written, one can then include instructions such as:
The algorithm for 'ProcessUserChoices' shows that the variable 'done' is initialized to false so we can use the code we just wrote above in 'ProcessUserChoices'. The next step is to declare the five instances of 'contract' just as we did in the first version of this program. This must be done here because it is no longer done in 'main'. Actually, we could have kept the declarations in 'main', but then we would have needed five parameters for this function - one for each of the instances. (Remember, this function cannot work without these instances.) You, as programmer, have a choice - you can declare the instances here in 'ProcessUserChoices' or declare them in 'main' and pass them to 'ProcessUserChoices'. Since 'main', as presently designed, has no use for these instances, it is better to declare them here.
There may be more variables to declare, and, as we discover them,
we will add their declarations. Here is the code so far:
Only the most recent versions of C++ compilers (as, for example, Borland C++ 5.0) have implemented the built-in Boolean type. This means that the programmer must declare his or her own boolean type, if the code is to work with earlier versions of C++. You will often find code that uses an earlier method for creating Boolean variables. In this method the programmer must declare his or her own boolean type. As you know, you have already been declaring your own types in the process of declaring new classes. The boolean type, however, does not have the complexity of say the 'contract' type - with a complex data structure and specific functionality - so we do not need to or want to use the class declaration mechanism to create the boolean type. C++ provides a much simpler mechanism, the typedef.
Here is the code:
const Boolean TRUE = 1;
const Boolean FALSE = 0;
Upon seeing the line
the compiler accepts the fact that there is a new type called 'Boolean' which at this point is nothing but a nickname for the type 'int'. (We will use the type name 'Boolean' to guarantee that we do not cause troubles with any compiler that already knows about the type 'bool'.) We then create two constants of this new type with the values 1 and 0. The constant 'FALSE' has the value 0 to match C++'s definition of 'false'. The constant 'TRUE' has the value 1 to match C++'s definition of 'true'. Two things to note:
The three lines
Here is the code for the file "common.h"
as built so far:
typedef int Boolean;
const Boolean TRUE = 1;
const Boolean FALSE = 0;
Of course, we have yet to declare a variable of type 'Boolean'. Remember, as we said earlier, there is a difference between declaring a type and declaring a variable. Type declarations simply present a description of a new concept. They do not declare that anything of that new type actually exists. It is a variable declaration that states that a new thing actually exists. Thus, no element of type 'Boolean' exists until the programmer adds a constant declaration as above or a line such as:
This last line could be used in place of the line
C. The 'if' and 'switch' Statements
Now we proceed to implement the algorithm - with many of the lines
being almost copies of those found in the algorithm. Here are
the first two:
Notice the simplicity of the test in the second line - it can be read as "while
not done....". Observe that there is no need for a relational
operator such as '==' or '!='. We could have written
or
Inside the loop we start with the line:
'GetOperatorChoice', of course, is one of the functions we have decided we needed but which we have yet to code. As we have seen before, there is no problem, at the coding level, with calling an undefined function as long as we write the definition later.
Notice that 'GetOperatorChoice' returns a value that is stored in a variable called 'choice'. Since this variable has not been declared, we better do so now, before we forget. Our analysis of 'GetOperatorChoice' indicates that it returns a character, so we declare 'choice' to be of type char:
The next part of the algorithm contains a rather complex set of embedded
'if' statements. A direct translation of this would look as follows
- with code added to handle the possibility that the user has
entered his or her choice in lower case letters:
bool done = false;
while (!done)
{ choice = GetOperatorChoice();
if ((choice == 'D') || (choice == 'd'))
{ contractNum = GetContractNum();
DisplayInfo(contractNum);
// If you notice a problem, see Part 'D' below
}
else
{ if ((choice == 'C') || (choice == 'c'))
{ contractNum = GetContractNum();
ChangeInfo(contractNum);
// If you notice a problem, see Part 'D' below
}
else
{ if ((choice == 'Q') || (choice == 'q'))
{ cout << "Do you really want to quit, (Y) or (N)\n";
cin >> answer;
if ((answer == 'Y') || (answer == 'y'))
{ done = true;
}
}
}
}
}
The variable 'answer' clearly will hold a 'Y' or an 'N' so it
should be of type 'char'. There is nothing new there. But, what
is somewhat new is where 'answer' is declared. Up to now, we have
talked about local variables as being variables declared inside
functions. Actually, a variable can be 'local' to any block
of code - where a block is any part of program between a matching
set of curly brackets ({
}). Since the variable, 'answer',
is only used in the block that deals with a users choice of 'Q',
it might be nice to make this variable local to this block. That
is what we did below.
if ((choice == 'Q') || (choice == 'q'))
{ cout << "Do you really want to quit, (Y) or (N)\n";
char answer;
cin >> answer;
if ((answer == 'Y') || (answer == 'y'))
{ done = true;
}
}
Be aware that we could just as easily have declared 'answer' up
with the other variable declarations. The only difference would
be that then 'answer' would be available to the whole function
and not just to the one block.
Once these declarations are added, the code works, although it
takes a bit of effort to write and is not easy to read. To handle
situations such as this, where there are a number of choices,
C++ has a new statement - the switch statement. It does
not add any power to the language - whatever one can write using
a switch statement, one could write using nested (embedded) if's
- but it does make the code easier to read and write. It is an
example of what some people like to call syntactic sugar -
because it provides no extra power but makes the code 'sweeter'.
Here is how the above code looks using this new statement:
while (!done)
{ choice = GetOperatorChoice();
switch (choice)
{ case 'D':
case 'd':
contractNum = GetContractNum();
DisplayInfo(contractNum);
// If you notice a problem, see Part 'D'
break;
case 'C':
case 'c':
contractNum = GetContractNum();
ChangeInfo(contractNum);
// If you notice a problem, see Part 'D'
break;
case 'Q':
case 'q':
cout << "Do you really want to want to quit (Y) or (N)\n";
char answ;
cin >> answ;
if ((answ =='Y') || (answ == 'y'))
{
done = true;
}
break;
default:
cout << "Invalid choice. Please try again";
} // end switch
} // end while
The line:
tells the compiler to concern itself with the contents of the
memory location symbolized by the variable 'choice'. Each of the
'case' lines tells the system what to do when this variable ('choice')
has a certain value. For example, when 'choice' contains 'c',
the code executes the lines:
In essence, "switch(choice)" acts as the first part of
an 'if' test condition and can be translated as "if (choice
==....)". Each case statement fills in the right hand side
of this relation. Thus, "case 'q'" fills in the relation
so that it reads, " if (choice =='q')".
OK, but what is this new break instruction? The 'switch'
statement has an interesting characteristic in that, unless told
otherwise, ALL lines below a certain 'case statement' are executed
until the end of the switch. In other words, if the break statement
is not there at the end of the instructions to be executed when
choice = 'c', the system will also execute the lines for the 'q'
case and the 'default' case. It is the break statement that tells
the system to skip to the end of the switch statement - the line
with the comment "// end switch".
This is why the lines containing only the word 'case' followed by some character, such as:
work. At first glance, you might think that the code is saying that NOTHING is to be done when 'choice' equals 'Q'. But, since there is no break statement here, the system 'falls through' and executes the lines corresponding to the "case 'q'" line until it reaches the break statement at the end of this case. Then it skips to the end of the switch statement. In essence, you can read:
as being equivalent to "((choice == 'Q') || (choice == 'q'))".
The next new element is the default statement. The original
algorithm did not bother to handle the situation where the user
enters some invalid character- in this case, neither 'd', 'c'
or 'q'. We will probably code 'GetOperatorChoice' to handle this,
using the "Clean Handling of Invalid Data" pattern, but it is common
practice to also have the switch statement handle invalid options.
If 'GetOperatorChoice' is coded correctly, the code we write in
the switch statement will never be used but such redundancy does
not hurt and since, as we can see, the switch statement makes
it easy to handle this situation, why not do so.
What the default statement signifies is that, if none of the case statements above the default statement is executed, the default statement itself will be executed. In other words, if the system does not enter one of the cases and hit a break statement causing the system to jump to the end of the switch statement, the code inside the 'default' statement is executed. In most situations, as in our situation, the default code simply announces that an invalid choice has been made. Note that there could be, but need not be, a 'break' statement here. We are already at the bottom of the switch statement, so if this option is taken, there is no code below that should not be executed.
Let's review how this whole section of code works. Having set 'done' to false,
the system automatically enters the 'while' loop. Then the user
is asked to enter a choice and that choice is compared with the
various options. If the user wants to display or change a contract,
the contract number is requested from the user and the appropriate
action is taken. If the user enters 'q' to quit, he or she is
asked to confirm that choice and if this choice is confirmed,
'done' is set to true.
As with all while loops, when the bottom of the loop is reached,
the code jumps to the top and executes the while loop test again. If
'done' is true, the loop ends and the system jumps to the first
line of code after the loop. Otherwise, the system enters the
loop again, causing the switch statement to execute. Note that
the switch statement, like the 'if' statement, is a one time deal
by itself. But, if it is coded inside a while statement, it can
be executed over and over. It is the 'while' that causes the repetition,
not the 'switch'!
You might want to consider the fact that we have another pattern here - one that is especially useful when there are a number of choices a user can make and each choice involves different code. This is the second new pattern in this chapter. Just a reminder: Take advantage of patterns like this whenever possible. It will make your coding life easier.
D. Passing a Class Instance as a Parameter
There is a problem with this code that we have ignored so far.
If you review what the design for the functions 'DisplayInfo'
and 'ChangeInfo' say these functions are to receive, you will
discover that they are to receive a contract, that is, an instance
of the class 'Contract'. The code we have written, however, passes
an integer, indicating which contract is to be used. Somehow, we
need to turn this contract number into a specific contract. Given
what we know so far, the easiest way to handle this is to again
use a switch statement - nested inside the switch statement handling
the choices.
'DisplayInfo' and 'ChangeInfo' will be handled the same way so
here is the code to call 'DisplayInfo':
switch (choice)
{ case 'D':
case 'd':
contractNum = GetContractNum();
switch (contractNum)
{ case 1:
DisplayInfo(contract1);
break;
case 2:
DisplayInfo(contract2);
break;
case 3:
DisplayInfo(contract3);
break;
case 4:
DisplayInfo(contract4);
break;
case 5:
DisplayInfo(contract5);
break;
default:
cout << "Invalid contract. Please try again\n";
}
break; // the break statement for case 'd'.
// The outer switch statement continues
Note that one can use the switch statement with both integers and characters. As a matter of fact, the material inside the parenthesis of a switch statement can be any expression that returns a single value. Technically, one could write something like:
and it would be valid as long as num1 and num2 were integers or
could be coerced into integers. By the
way, when a variable of type 'char' is in the parenthesis, it
is technically turned into an integer and treated as an integer
code.
We finally have code that works, but there is one potential improvement we can make. Suppose the user wishes to display the information on many contracts. As we have written the code, this user will have to re-enter a 'D' and then the new contract number. That's OK, but some might argue that a user displaying contracts will often want to display many contracts at once, and that being forced to repeatedly re-enter a 'D' is a sign of poor design. To avoid this, we could add a loop that allows the user to enter contract numbers until a zero is entered as a sentinel value. Our interface analysis did not discuss this, but now that we are dealing with implementation details, the value of this is clearer. Here we have an example of the interplay of analysis, design, and implementation.
What pattern
would one use to implement this change? Take a moment to answer this.
Once again we would use the "While Loop With Priming
Read" pattern. Now the code code (for the 'DisplayInfo' option) would look as follows:
switch (choice)
{ case 'D':
case 'd':
contractNum = GetContractNum();
while (contractNum != 0)
{ switch (contractNum)
{ case 1:
DisplayInfo(contract1);
break;
case 2:
DisplayInfo(contract2);
break;
case 3:
DisplayInfo(contract3);
break;
// The two other cases go here
default:
cout << "Invalid contract. Please try again\n";
}
contractNum = GetContractNum();
}
break; // the break statement for case 'd'.
// The rest of other cases goes here
}
The code here really is on the verge of being too complex. Later, we will see how 'ProcessUserChoices' can be simplified via further functional decomposition.
E. Receiving Class Instances as Parameters
Our analysis indicates that 'DisplayInfo' receives a contract
and returns nothing. Therefore the declaration looks as follows:
Note how we use 'Contract' (the name of the class) where a type name goes. This is another reminder that class names are also type names. Thus, we are declaring 'contract' (lower case 'c') to be an instance of class 'Contract' (upper case 'C').
You might be concerned that this instance declaration does not include any initialization of property values as have the other Contract declarations in this chapter. However, Since we are declaring a parameter here and the system
knows that this local instance will be immediately instantiated
with the property values of some contract on the calling side,
this usage is OK. What actually happens is that the properties
of the actual parameter are copied to the corresponding data members
in the local instance.Later we will see situations in which it
is necessary to explicitly declare and define what is called a copy constructor
The definition is relatively straightforward. We simply need to request each piece of information from the instance by calling the appropriate member function and then display it. Recall from the last chapter that we call a member function by typing the instance name, followed by a dot (.), followed by the member function name. Here then is the code:
We have added three 'endl' outputs at the end to cause three lines
to be skipped on the monitor between this output and whatever
happens next. This simply makes the output more readable.
There is a slightly shorter way to write this code. Functions
that return values can be placed anywhere a variable can be placed,
as long as one is careful to match types. For example then, one
could drop the variable 'sqFootage' and replace it directly with
the function call as in:
cout << "\n\nThe square footage for the contract is: "
<< contract.ProvideSquareFootage()
<< endl;
Using this approach, the value returned by 'ProvideSquareFootage'
is sent directly to the output stream, instead of being temporarily
stored in a local variable whose value is then sent to the stream.
You should use whichever approach feels best to you. (Note that
in this version the function call does not end with a semi-colon
because it is part of a larger instruction.)
F. Returning a Class Instance as a Parameter
The declaration for 'ChangeInfo' is different from that of 'DisplayInfo'
in that 'ChangeInfo' returns a contract. Therefore, we could write
the declaration as follows:
If we did this, however, we would have to rewrite the calls to "ChangeInfo' in 'ProcessUserChoices' to, for example:
In other words, we would have to pass 'contract1' via the parameter list and get back the changed contract via the function return mechanism.
While this would work, it is not as commonly done as a second
way. In chapter four we learned how to use the '&' symbol
to allow more than one value to be returned. We noted further
that the '&' symbol creates a "two way transmission path",
allowing information to not only be returned but also sent via
the same connection. In the case of 'ChangeInfo' we already need
a path to transmit information from the calling function (in other
words, we need to receive a contract) so why not simply use the
same path to return the changed contract. The '&' does this.
Here is the new declaration:
To emphasize the key point: The '&' symbol creates a two way path so that the original information about the contract in question can be received by 'ChangeInfo' and the changed information can be returned. When a call to 'ChangeInfo' such as:
in 'ProcessUserChoices' is completed, the information originally
in contract1 has been sent to 'ChangeInfo' and the same information,
as modified by 'ChangeInfo', has been sent back. The instance 'contract1'
in 'ProcessUserChoices' has now been modified.
We now move on to the definition for 'ChangeInfo'. This time,
instead of calling 'Provide.... member functions as we did in
'DisplayInfo', we call 'Change....' member functions to cause
the properties to be changed. Before doing that, however, we must get the new value(s) for the property(ies) to be changed.This, in turn, requires that we ask the user which property(ies) she or he wants changed. To make the user interface more efficient, we allow the
user to change as many properties as she or he wants - using a
sentinel value of 'N' to indicate completion. Here is the code:
void ChangeInfo(Contract& contract)
{
char property;
property = GetProperty();
while ((property != 'N') && (property != 'n'))
{ switch (property)
{ case 'S':
case 's':
int sqFootage;
sqFootage = GetSquareFootage();
contract.ChangeSquareFootage(sqFootage);
break;
case 'K':
case 'k':
int numDesks;
numDesks = GetNumDesks();
contract.ChangeNumDesks(numDesks);
break;
case 'Y':
case 'y':
int numDays;
numDays = GetNumDays();
contract.ChangeNumDays(numDays);
break;
default:
cout << "\nInvalid choice of properties. Please try again\n";
} // end switch
property = GetProperty();
}
}
One other point: The while test condition is a bit different in
that it uses the 'AND' symbol ('&&'). Students are often
confused about when to use AND and OR. The confusion comes partially
from the way a situation like this is commonly described in English.
The specifications are likely to say something like:
While the specifications statement has an 'or' in it, it gets
translated as 'AND' or '&&'. The reason is that the while
statement is not concerned with when to stop as when to continue.
Remember, the while statement test condition can be translated
as:
In other words, when translating a phrase such as "Stop when the
user enters 'N' or 'n'" into a C++ while statement, it might
be wise to first translate the English into, "while the user
has not entered 'N' and has not entered 'n'". Note how
this English captures the meaning of the C++ 'while' statement
and, therefore, the translation process (from algorithm to code)
is relatively straightforward. The moral of the story is: Be very
careful how you translate English 'and' and 'or' into '&&'
and '||'.
Since this translation process can be quite tricky, here is another way to handle it. Above we noted that the specifications usually describe the conditions under which a loop will stop. We then observed that a 'while' loop is coded in terms of the conditions under which the loop will continue. Continuing is the opposite of stopping so we can apply something called DeMorgan's law. If you have studied this law, you might want to remind yourself about what it says. However, you don't need to know anything about it to apply the algorithm below:
| |
Main Menu | Next Section |