| 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 | Section VIII: Expanding the Idea of Classes |
|
Table of Contents
Learning C++:
An Index of Entry Points
2. The A reference document on the basic elements of C++.
3. The Patterns
|
A. Declaring the Instances 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:
While we are at it, we might as well calculate the Per Week Charge:
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. And the new charges: 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'
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'
#include <iostream.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() Be sure you understand that five distinct memory areas have
been set aside, each of which represents a separate instance of the
'contract' class with its own memory locations holding the values for the
three properties. (Remember, these properties are formally called 'data
members'.) To ensure that you understand this notion, here is a
representation of memory containing these five instances.
B. Using the
Change.... Member Functions to Initialize the Instances 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:
contract1. ChangeSquareFootage(1000);
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:
Figure 5.3 shows how the memory for the contracts would look after this
code is executed.
C. Using the Provide.... Member Functions
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! Quite repetitive isn't it. Note that we can use the same
variables over and over because once we have output the value of a
variable, the value is not useful any more in this case. Note also how the
data members of each class are encapsulated. We cannot write code such
as: 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 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 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" )
|