cs1ch5sec6.htm
 
 
CHAPTER 5
 
Object-Oriented Design
 

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

of C++

A reference document on the basic elements of C++.



3. The Patterns





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:

  1. Instantiate the five instances
  2. Use the 'change' functions to replace the garbage values in the data members of each instance with values of our choosing
  3. Display the property values for each instance. This will test the 'change' and 'provide' functions.
  4. Change the property values for each instance.
  5. Display the property values again. These last two steps will test the 'change' functions a second time.

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:

double myNumber;

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:

Contract contract1;

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.

    // 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;
      .
      .
    }

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
As Figure 5.2 shows, the five instances all contain garbage. To initialize these instances to the property values we decided on above, we can use the 'Change... functions. Think of these instances as animate objects and that we are going to send each instance a series of messages telling it the values of its properties. Here are the names for the messages we want to send:

    ChangeSquareFootage
    ChangeNumberOfDesks
    ChangeNumberOfDays

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:

ChangeSquareFootage(1000); // Illegal

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:

contract1.ChangeNumDesks(3);

To change the number of desks for contract2, we would write:

contract2.ChangeNumDesks(10);

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:

    ProvideSquareFootage
    ProvideNumberOfDesks
    ProvideNumberOfDays
    ProvidePerWeekCharge

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:

int sqFootage;
sqFootage = ProvideSquareFootage();    // Illegal

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:

sqFootage = contract1.ProvideSquareFootage();

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:

cout << "The square footage of contract1 is: " << sqFootage << endl;

to output the square footage returned.

Similarly, to retrieve and output the per week charge for contract 1 we could write:

    double charge;
    charge = contract1.ProvidePerWeekCharge();
    cout << "The per week charge for contract 1 is: " << charge << endl;

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;
    }
    

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:

cout << squareFootage;    //Illegal

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:

cout << contract1.squareFootage;    //Illegal

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:

contract1. ChangeNumDesks(4);

The calls to the others look the same:

contract2. ChangeSquareFootage(600);
contract2. ChangeNumDays(5);
contract3. ChangeSquareFootage(300);
contract4. ChangeNumDesks(25);
contract4. ChangeNumDays(3);

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" )

Top of Section Main Menu Next Section