| |
Main Menu | Next Section |
| Section VI: Using Class Declarations in a Program | Section I: The Object-Oriented Paradigm | Section II: Class Analysis and Design | Section III : CRC Cards |
| Section IV: Classes in C++ | Section V: Defining Classes | Section VII: Constructors |
A. Declaring the Instances
Looking back at the problem narrative for this class, we
recall that Olympia wants to keep track of five contracts which
means she should be able to get or change the information about
these five contracts. The code we have written so far provides
the data structure and functionality to do this but it does not
provide Olympia with any way to take advantage of what we have
written. It is now time to design and write the code for a program
that that will use the contract class to meet Olympia's needs.
The design process has turned up one class with a number of basic
functions and properties. A program that used this class to give Olympia all the
capabilities she would like could become quite complex. Let's
start, however, with a simple program that really only tests the
functionality of our design. Such a program is called a driver
program. It is not really useful in itself except as a test vehicle.
Since such a test program is not meant to be used outside the program development process, it does not require the detailed analysis and design process we developed in chapter three. It does, however, require a careful analysis and design in relation to what will be tested and how. Here, we will simply use a basic, common testing methodology and avoid the detailed analysis. Be aware, however, that proper testing of programs is a key and often neglected element of good program development. (The number of bugs in many pieces of software testifies to the failure of many to realize this.)
One straight forward way to test the functionality of a class is to exercise (use) the various member functions. In our case then let's create five instances of 'contract' and implement the following steps:
Notice that key to testing like this is deciding beforehand what the results should be at each point in the program. In this case, we first need to determine what information (what values for each data member) should be placed in each instance as a result of step two above. Those same values should be output as a result of step 3. We then need to determine how we are going to change each of those values and look for the new values as output. In the case of the Per Week Charges, we will need to do our own calculating in order to compare the expected outputs with the actual outputs. In many ways this is like the trace process we have already seen. The big difference, of course, is that we are comparing the expected results with the computer's results as opposed to with results created by our own processing of the instructions.
We have five instances to create so we need five sets of property
values to place in them. As usual, we want to use values that are easy to work
with. Consider the following as initial values:
| Square Footage | Number of Desks | Number of Days | |
| Contract 1 | 1000 | 3 | 5 |
| Contract 2 | 500 | 10 | 3 |
| Contract 3 | 200 | 1 | 4 |
| Contract 4 | 2000 | 20 | 5 |
| Contract 5 | 300 | 2 | 2 |
While we are at it, we might as well calculate the Per Week Charge:
| Contract 1 | Contract 2 | Contract 3 | Contract 4 | Contract 5 |
| $325 | $225 | $60 | $1000 | $50 |
Step four involves the changing of property values but there is no need to change the values of all the data members of all the instances. The numbers in the table below represent
all the changes we will make.
| Square Footage | Number of Desks | Number of Days | |
| Contract 1 | 4 | ||
| Contract 2 | 600 | 5 | |
| Contract 3 | 300 | ||
| Contract 4 | 25 | 3 | |
| Contract 5 |
And the new charges:
| Contract 1 | Contract 2 | Contract 3 | Contract 4 | Contract 5 |
| $350 | $400 | $80 | $675 | SAME |
Ordinarily we would now view the requirements of the main program
to see how the task might be decomposed - as we did in the examples
in the previous chapter. To avoid a few issues we will not do
that immediately. Instead, all the code will be included in 'main'.
First, we start with our comments:
// Program to test the functionality of the class 'contract'
// File: ch5tst1.cpp
Note that we call what we are writing here a 'program'. The other
two files we have created in this chapter were not in themselves
programs. They were parts of or tools to be used by programs,
including this test program.
Next come the include files. To make our testing easier there will be no inputs into this program. However, the program will output the values of the data members of the five instances so we need to include iostream.h. As you might expect, the program also needs to know about the declaration for class 'Contract', so 'contract.h' must also be included. Here is the code so far:
// Program to test the functionality of the class 'contract'
// File: ch5tst1.cpp
#include <iostream.h>
#include "contract.h"
void main()
{
.
.
}
Note that the built-in header file (iostream.h) uses the 'angle
brackets ('<', '>') in the include statement while the
header file we created (contract.h) uses double quote marks. Standard
header files such as iostream.h are usually stored in a predetermined
directory or set of directories. The angle brackets tell the system
to look in the directory or directories set up to hold the built-in
header files. The quote marks tell the system to first look in
the directory that contains the file being compiled and only if
the file to be included is not there, look in the predetermined
directories.
Instructions such as #include are actually handled by a
preprocessor - a program that looks for and handles all
instructions beginning with the '#' symbol BEFORE the compiler
begins its work. Thus, the #include instruction brings in the
code for 'contract.h', for example, and when the compiler starts,
it sees that code as if it were part of the file being compiled
- the file 'ch5tst1.cpp' in this case.
Now for the code for 'main'. Our first task is to declare (and
define) five instances of class contract. Earlier (section
IV.A
) we stressed the point that a class is nothing but a complex
type. In fact, when it comes to declaring the instances of a class,
we use the same syntax as we used to declare an 'instance'
of type double or int or ....
As you know, when we write a line such as:
we are not only declaring that a variable with the name 'myNumber' exists in the program, we are also setting aside a specific memory location for this variable - we are defining the variable. As it stands, the memory location for this variable contains garbage at this point.
Similarly, if we write the line:
we are requesting that the system set aside enough memory for a contract, give that memory area the symbolic name 'contract1', and don't put anything into that memory. In other words, let the memory hold whatever garbage is left over from its last use.
In all prior variable declarations (and 'contract1' is now a variable)
we would picture the memory set aside as a box big enough to hold
a value of whatever type was being declared. Doubles required
larger boxes than integers which required larger boxes than chars.
For variables that are instances of some class we need to think
of memory as a big box with compartments - one compartment for
each property. The compartments, of course, will be of different
sizes, depending on whether the properties are of type int, char,
or double - or even of some class. (Because classes are essentially
new types, they can be used wherever types are used and, just
as we can use 'if' statements inside 'whiles' inside 'if's etc.,
we can use classes as type names inside class declarations.)
Back to our instantiations of contract, here is our code after
declaring the five instances.
void main()
{

These, of course, are the names of the member functions whose purpose is to change the information of a specific property value. We will use them here to change the garbage values held by the data members of each instance.
To use these functions we can't just write, for example:
as we might have done with regular functions in chapter four. Member functions always work with specific instances of a class but in this code the computer has no way of knowing for which instance we want to change the square footage.
Suppose we want to change the square footage first for the instance 'contract1' - a logical choice. The code for this would be written:
This is a call to the function 'ChangeSquareFootage' associated withthe instance 'contract1'. We can read this as "Send a message to contract1 to change its square footage to the value being passed as a parameter, that is to 1000. (The 1000 is the square footage we decided above that contract1 would have. All the numbers in the examples below will also come from that same source.)
Similarly, to change the number of desks for contract1 we could write:
To change the number of desks for contract2, we would write:
Here we are sending the same message but we are sending it to contract2.
The code to initialze all the data members of all the instances is similar. It would be placed right after the five declarations as in:
void main()
{ Contract contract1;
Contract contract2;
Contract contract3;
Contract contract4;
Contract contract5;
// Initializing contract1
contract1.ChangeSquareFootage(1000);
contract1.ChangeNumDesks(3);
contract1.ChangeNumDays(5);
// Initializing contract2
contract2.ChangeSquareFootage(500);
contract2.ChangeNumDesks(10);
contract2.ChangeNumDays(3);
// Initializing contract3
contract3.ChangeSquareFootage(200);
contract3.ChangeNumDesks(1);
contract3.ChangeNumDays(4);
// Initializing contract4
contract4.ChangeSquareFootage(200);
contract4.ChangeNumDesks(20
contract4.ChangeNumDays(5);
// Initializing contract5
contract5.ChangeSquareFootage(300);
contract5.ChangeNumDesks(2);
contract5.ChangeNumDays(2);
.
.
}
Figure 5.3 shows how the memory for the contracts would look after this code is executed.
C. Using the Provide.... Member Functions
OK, so now we have initialized our five instances! How do we display their property values to test that the declarations and Change functions worked properly? (Remember, the sole purpose of this program is to test our code.) We can again think of these instances as animate objects and that we are going to send each instance a series of messages, asking it for the values of its properties. Here are the names for the messages we want to send:
These, of course, are the names of the member functions whose purpose is to return (provide) the specific information asked for. You might object that the "per week charge" is not a property. It is true that it is not a data member of the instances, but it is a value that we, as designers of the class 'contract', have agreed to provide to any program that creates instances of the class. It is irrelevant to users of this how the information is given, just that it is given on demand.
(By "users" we again mean programmers who take advantage of the design and code created for this class by including the code in their programs. Since, at this point, you are acting as designer of all classes and all programs that use the classes, this may be hard to grasp. After all, you know all the details of all aspects of your code. Try to imagine a situation where hundreds of programmers are involved in designing dozens of classes to be used in numerous different programs. Or, imagine yourself re-using a class you designed and coded but whose inner workings you have long since forgotten.)
In any case, as before, to use these functions we can't just write, for example:
As was said above, member functions always work with specific instances of a class so in this code the computer has no way of knowing from which instance we want to get the square footage.
Suppose we want the square footage first from the instance 'contract1'. The code for this would be written:
This is a call to the function 'ProvideSquareFootage' associated with the instance 'contract1'. We can read this as "Send a message to contract1 to provide its square footage and store what contract1 returns in the memory location symbolized by the local variable 'sqFootage'."
We could then write the code:
to output the square footage returned.
Similarly, to retrieve and output the per week charge for contract
1 we could write:
To stress a point: note how the code for this 'provide' instruction
has exactly the same form as the previous one. Only the class
itself knows that these two functions do something different.
The code for all the displays looks very similar - ah ha, another
pattern!
// Program to test the functionality of the class 'contract'
// File: ch5tst1.cpp
#include <iostream.h>
#include "contract.h"
void main()
{ Contract contract1;
Contract contract2;
Contract contract3;
Contract contract4;
Contract contract5;
// Initializing contract1
contract1.ChangeSquareFootage(1000);
contract1.ChangeNumDesks(3);
contract1.ChangeNumDays(5);
// Initializing contract2
contract2.ChangeSquareFootage(500);
contract2.ChangeNumDesks(10);
contract2.ChangeNumDays(3);
// Initializing contract3
contract3.ChangeSquareFootage(200);
contract3.ChangeNumDesks(1);
contract3.ChangeNumDays(4);
// Initializing contract4
contract4.ChangeSquareFootage(200);
contract4.ChangeNumDesks(20
contract4.ChangeNumDays(5);
// Initializing contract5
contract5.ChangeSquareFootage(300);
contract5.ChangeNumDesks(2);
contract5.ChangeNumDays(2);
// Code to test the declarations and initializations
int sqFootage;
int numDesks;
int numDays;
double charge;
sqFootage = contract1.ProvideSquareFootage();
cout << "The square footage of contract1 is: " << sqFootage << endl;
numDesks = contract1.ProvideNumberOfDesks();
cout << "The number of desks of contract1 is: " << numDesks << endl;
numDays= contract1.ProvideNumberOfDays();
cout << "The number of days of contract1 is: " << numDays << endl;
charge = contract1.ProvidePerWeekCharge();
cout << "The per week charge for contract 1 is: " << charge << endl;
sqFootage = contract2.ProvideSquareFootage();
cout << "The square footage of contract2 is: " << sqFootage << endl;
numDesks = contract2.ProvideNumberOfDesks();
cout << "The number of desks of contract2 is: " << numDesks << endl;
numDays= contract2.ProvideNumberOfDays();
cout << "The number of days of contract2 is: " << numDays << endl;
charge = contract2.ProvidePerWeekCharge();
cout << "The per week charge for contract 2 is: " << charge << endl;
sqFootage = contract3.ProvideSquareFootage();
cout << "The square footage of contract3 is: " << sqFootage << endl;
numDesks = contract3.ProvideNumberOfDesks();
cout << "The number of desks of contract3 is: " << numDesks << endl;
numDays= contract3.ProvideNumberOfDays();
cout << "The number of days of contract3 is: " << numDays << endl;
charge = contract3.ProvidePerWeekCharge();
cout << "The per week charge for contract 3 is: " << charge << endl;
sqFootage = contract4.ProvideSquareFootage();
cout << "The square footage of contract4 is: " << sqFootage << endl;
numDesks = contract4.ProvideNumberOfDesks();
cout << "The number of desks of contract4 is: " << numDesks << endl;
numDays= contract4.ProvideNumberOfDays();
cout << "The number of days of contract4 is: " << numDays << endl;
charge = contract4.ProvidePerWeekCharge();
cout << "The per week charge for contract 4 is: " << charge << endl;
sqFootage = contract5.ProvideSquareFootage();
cout << "The square footage of contract5 is: " << sqFootage << endl;
numDesks = contract5.ProvideNumberOfDesks();
cout << "The number of desks of contract5 is: " << numDesks << endl;
numDays= contract5.ProvideNumberOfDays();
cout << "The number of days of contract5 is: " << numDays << endl;
charge = contract5.ProvidePerWeekCharge();
cout << "The per week charge for contract 5 is: " << charge << endl;
}
hoping to get access to the data member of some instance without making any function calls. This fails for two reasons. First, each instance has its own memory location called 'squareFootage' so how does the computer know which instance we want? Second, the data members are encapsulated so, even if there was only one instance, we would not be able to get direct access to its data members.
Could we fix the problem by writing:
as we did with function members to indicate which instance we
are interested in? Note that this does indicate which instance
we are interested in, BUT it has no effect on the encapsulation
issue. The only way to get access to those parts of a class that
are declared to be private is through the appropriate member functions.
This is a bit like walking into a post office to pick up a package.
You may know that a package is somewhere 'behind the counter'
but you do not have direct access to it. You must ask a clerk
to go back and get it for you.
D. Changing Property Values
The next step we decided to take in our test program was to change
the property values of some instances. Here is the table again
of changes. (Remember, blanks in the table refer to properties
that will not be changed):
| Square Footage | Number of Desks | Number of Days | |
| Contract 1 | 4 | ||
| Contract 2 | 600 | 5 | |
| Contract 3 | 300 | ||
| Contract 4 | 25 | 3 | |
| Contract 5 |
There are six changes that need to be made so we have six function calls to write. The form of the calls is the same as in the previous set of change instructions.To change the 'number of desks' value in contract 1 we write:
The calls to the others look the same:
Note that contracts 2 and 4 each have two function calls (are
sent two messages) because two property values are changed. Note
also that we cannot directly change the per week charge for any
instance. Since there is no data member for this 'property', no
member function is included to directly change its value. However,
any change to any of the other three properties will result in
a change in the per week charge when it is re-calculated.
To make sure that these changes work, we next output the four values
of the properties of the five instances. This is simply a repeat
of the code you saw above. You are urged to complete this yourself.
Include the necessary code to output the property values of instance
five, even though no changes were made to it, in order to make sure
your code does not accidentally make changes it should not make.
You should then run this program and see if the outputs generated
are what you expect - see "Putting it All Together"
below. When you are finished, check out the authors' version
of this code under the file names, "contract.h", "contract.cpp", and "ch5tst1.cpp"
E. Putting It All Together
Prior to this, we used functional decomposition to break up the code. However, we kept all the code in one file with the exception of some built-in header files. Now we have split up the code into two separate '.cpp' files and one header files. To allow our program to function as a single unit, we need to bring these files back together.
We have already seen how to accomplish part of this through the use of header files and the include statement. As we said, the "#include " statement copies the code in the included file into the file containing the "#include" statement. This is standard in all versions of C++. However, bring together or linking the .cpp files is not so standard across C++ packages.
First, we need to understand
a bit about the C/C++ heritage and approach to programming. It
has always been considered very important in the 'C' world that
one be allowed to compile parts of a program separate from other
parts. Such separate compilation is especially important
in large programs, where to compile a full program could take a
hour or more. No one wanted to wait that long to fix one small
change to one small part of a program.
Separate compilation avoids this. Each program file containing
definitions can be compiled into machine code by itself, with references
to code found in other files finalized later through the linker.
In C++ the header files with their declarations act as promises – that the definitions for the functions declared in the header file are available and will be found at link time.
Separate compilation does, however, make the whole process more
complex. Because one file may or may not depend on other files,
it is not always clear to the compiler what should be re-compiled.
The traditional approach involved what are called 'make' files
in which the programmer explicitly stated what files depended
on what other files. Using this information, the compiler would
re-compile a given file only if that file itself had been changed
(based on the date and time of the file on disk) or if one of
the files it depended on had been changed.
More recently, easier to use, more automated approaches have surfaced. Using a windows-mouse interface (often called a Graphical User Interface or GUI), the programmer defines the dependencies with very little typing. The dependencies may be displayed via a simple indenting scheme, making it easy for the programmer to visualize the dependencies. Such tools also allow the programmer to compile the same code for 'DOS' or 'Windows' and for 16 or 32 bit systems. You are urged to check the manual for your programming environment to determine the easiest way to perform separate compilation, etc. on your own system. (Borland C++, for example, allows programmers to use what are called projects. The general instructions for working with projects in Borland can be found in the document "BorlandProject.htm" )
| |
Main Menu | Next Section |