|
Table of Contents
Learning C++:
An Index of Entry Points
2. The A reference document on the basic elements of C++.
3. The Patterns
|
I. Introduction
For many years this process of functional decomposition was the
main tool programmers used to handle complexity. It made a certain
amount of practical sense because, as we noted in chapter 3,
humans do often handle task complexity by decomposing tasks into
smaller tasks. More recently, however, a new approach for analyzing
a complex problem and designing a solution has emerged.
This new design methodology is based on the object oriented
programming (OOP) paradigm (a paradigm is a pattern or way
of looking at things). The object oriented paradigm says that
the world can be best analyzed by focusing not on the tasks but
on the objects found in the world.
Back in chapter one we argued that Computer Science can be seen
as a science of modeling where we study approaches to modeling
or capturing some element of our real or imaginary world(s) in
a computer program. To create a computer model of some world we
need to analyze and come to an understanding of that world. Perhaps
then, it is worth studying how humans analyze and describe (create
a model of) the elements of our existence. Although humans do
perform functional decomposition when faced with a task to accomplish,
they are much more likely to look at the world itself in terms
of the objects (things) they see in that world.
Consider the idea of a school. While there are all kinds of actions
being performed in a school, if you ask someone to describe what
a school is, they are more likely to start with the objects that
exist in a school: teachers, students, classrooms, tests, homework,
etc. than the activities. Of course, part of the process of describing
these objects includes stating what they do. It is not that actions
are ignored. They are simply not the first aspects to be observed
and commented on.
Proponents of the object oriented paradigm argue that programming
problems can be best analyzed by focusing first on the objects
involved in a problem and only then on the functions performed
by and on those objects. It is not so much a matter of which
is better but, simply of which comes first - the object analysis
or the task analysis. Indeed, we shall see that all the work we
did with functions in the last chapter will be very important
in our efforts in this chapter.
The idea then is that when a programmer is given a new problem,
the decomposition process that initially takes place is in terms
of the objects found in the problem. These objects can be classified
into different groupings called classes and these classes
can then be analyzed to determine their properties, the
actions they are required to perform, and their relationships
with other classes. A specific object is an instance of
a class and has all the properties, actions, and relationships
determined for the class.
The notions of 'class', 'property' and 'instance' as found in
the object oriented approach, are not new concepts. Humans have
been decomposing/dividing the world up into object/classes for
as long as they have been wondering about the world. One strong
example of this is Biology where the complexity of life around
us has been studied extensively in terms of classifying life forms
into various groups and subgroups. The basic group is the species.
Using the object oriented terms of class and instance, a species
can be seen as a class and an individual member of the species
as an instance of the class, an object belonging to the class.
Thus, you as an individual are an instance (member) of the species
(class) Homo Sapiens. You therefore have the properties and know
how to perform the actions known to any instance of the class
Homo Sapiens.
The strength of this approach for the study of life is that a
biologist can focus on the commonalties found in all members of
a species, for example, Homo Sapiens, and not worry about the
idiosyncrasies of a particular instance of the species, as in
the unique characteristics of a particular human. Same for the
programmer - he or she can design a class by focusing on the common
properties and actions of the class. Later, all instances of the
class (objects belonging to the class) can take advantage of that
design.
The OOP paradigm applies this notion to a specific programming
task by trying to discover the objects involved in the programming
task, the classes into which those objects can be grouped, and
the relationships among classes. As part of this analysis the
OOP paradigm seeks to determine what are the common properties
(characteristics) of all instances of each class and what actions
the instances of each class are capable of. From this, one can
design the necessary classes.
The OOP paradigm goes one step further with its ideas of class
and object. The properties of an object belong to the object itself
and should not easily be changed by other objects. It is not that
the properties can't be changed but the change should be controlled.
From a programming perspective the idea of 'controlled changes'
helps insure that variables in a program do not get changed except
when such a change is really desired by the programmer. Local
variables and parameter passing in functions were a first step
to ensure 'controlled change' and the object paradigm extends
this idea. Since your only experience with programming may be
what you have done so far with this text, this issue may not be
clear. If so, you need to trust that one way to support the production
of 'bug free' code is to control when values get changed.
In the OOP approach 'controlled change' is implemented through
encapsulation. What encapsulation means is that the properties
of an object, as defined in the class, are 'inside' the object
and cannot be easily accessed or changed by other objects. As
we will see, to change encapsulated properties one tells the object
itself that the change should be made and the object handles the
actual change.
II. Class Analysis and Design
You have spent quite a bit of time already designing algorithms
so you should have some sense of how to handle the sixth step.
Remember to apply all that you have already learned as we proceed
into this new territory. In this chapter we will spend a lot of
time focusing on steps one through five as they are fundamental
to a successful object-based programming effort. The key to these
steps is that they describe what is required of the program
without saying anything about how. It is important to
not worry about how you are going to accomplish a task until you
are sure you know what it is you are trying to accomplish. Even
step six which does focus on the 'how' of each function member
does so at a high level - the algorithmic level - and there is
still no concern with syntactic details of a specific programming
language.
Once the design of the various classes required for a program
is complete, one still must determine how the instances of the
various classes, the actual objects, are to be used. There must
be some part of the program that coordinates the activities of
the various objects. In our programs this coordinator will be
the function 'main' (the same 'main' we have already seen and
used) and the functions associated with it in the same file.
The last phase of the design stage, therefore, involves designing
'main' and any functions it uses that are not part of specific
classes.
This last part will differ only slightly from what we have done
so far. One must determine the variables and functions required
to actually use the objects found in the problem and design algorithms
for any of those functions that are complex. Note that these functions
do not belong to a specific class. For that reason they are referred
to as 'non-member functions'. All the functions we have
worked with so far have been 'non-member' functions. Since we
were not using classes and had no idea what 'member' functions
were, there was no reason to talk about non-member functions.
Sometimes the challenge in object-oriented analysis and design
is in discovering the classes necessary for a program. Sometimes
the challenge is in finding appropriate properties of the class(es)
(What should an instance of class 'person' include - a name property?
an age property? a date of birth property?, the grandfather's
name? ....). Sometimes the challenge is in determining the correct
set of actions (member functions/methods) the class(es) should
be capable of handling. Most often the challenge is in all of
these.
Although the five steps of the object oriented analysis and design
process are described separately below, they often come together
in the actual process of designing a program. And, as with functional
decomposition, it is important that you complete the analysis
and design before proceeding to the code.
In other words, when you first start the analysis and design process,
be careful to focus only on what objects are involved and what
the program is to do. Don't worry about the how of the programming
problem and certainly don't ask yourself if you know how. All
that comes later.
III. The Analysis Phase
The words you underline in the narrative represent possible classes,
objects and member data as well as elements that are not part
of any particular class/object. Our task is to decide which of
the underlined nouns represent objects (or classes of objects),which
nouns represent parts of objects/classes (the member data) and
which nouns represent elements that are not part of any class.
For example, consider a problem narrative that includes the sentences:
It would be logical, given our common sense understanding of
persons, names, and addresses, to say that the word person represents
a class , that Fred, John, and Mary are objects (instances of
the class 'person'), and that name and address are properties
(data members) of the same class. It would also be logical to
say that 'total' is not part of the class 'person' but it may
be an important part or element of the problem.
Not all cases are as easy as this one but you should use your
common sense and understanding of the world you are trying to
model (write a program for) to help you distinguish between objects,
classes and properties of objects.
Note that a class is a general description of an object or set
of objects. In the example above, the sentences talk about a number
of individuals. We then have a number of objects but each can
be represented by the same class, Person. Our goal then is to
use the discovery of the objects that exist in a program to discover
the classes necessary to represent all the objects involved.
The words circled in the problem narrative represent possible
member or non-member functions. To find member functions look
for verbs describing actions required of an object or class.
As a general rule, these include actions that ask for, change,
or use the values of the data members of a class.
IV. CRC Cards
In the CRC approach a design team gathers together with a stack
of index cards. Using the underlined nouns in the problem narrative
the team comes up with the objects/classes required in the program
and writes down a name for a specific class in the upper left
corner of a card - one card for each class. A line is then drawn
down the middle of the card and on the left we use the circled
verbs to determine the responsibilities of the class - what actions
will objects belonging to this class perform. On the right side
we list other classes that this class must use in order to carry
out its responsibilities. Such required classes are called collaborators.
Thus we see where the name CRC comes from - Classes, Responsibilities,
Collaborators.
Notice the words 'responsibilities' and 'collaborators'. These
have been chosen on purpose to emphasize that each object (instance
of some class) is to be considered as an 'actor'. Objects 'do'
things. They receive and respond to messages or requests
sent to them by other objects or 'main' and its functions.
This may seem a bit strange at first - as if we are anthropormorphizing
our programs. For the fun and ease of it, you might want to imagine
a program as some kind magical land where inanimate objects come
to life.
Consider as an example, a race car game played on a computer.
An object-oriented program for such a game would probably consist
of a race track class with one or more object instances representing
possible race tracks and a race car class with one or more instances
representing the various cars involved in a race. We humans do
actually use phrases like the 'response' of the car, and we can
think of an instance of the class 'car' in this program as 'responding'
to messages to accelerate or brake or turn. In other words, each
instance of the class 'car' must be responsible for accelerating,
braking, and turning itself. The responsibilities of an instance
of race track might include displaying the race on the screen,
keeping track of such things as who is in the lead and the elapsed
time of the race, and of informing a car when it has crashed into
a wall of the track.
This last may seem strange but again, remember the magic land
we are in. Perhaps one should actually have a class wall where
some set of instances of wall form part of an instance of track,
and instances of wall inform instances of car when a car has crashed
into a wall. Walls would be collaborators of track because a track
would not know if there was a crash (something it needs to know
if it is to accurately display the race) unless it asked the walls
of a track. (If this seems a bit complex and far fetched, don't
worry much about it yet. The details will come later.)
Back to our CRC cards, note that this approach does not directly
determine the data members or properties of a class. The assumption,
as we will see below, is that some collaborators (such as walls
in relation to tracks) will become data members (tracks have walls)
and that other data members will become clear as we consider which
underlined words in the problem narrative are useful to the behaviors/responsibilities
of the class.
V. Discovering the Classes Necessary for a Programming Problem
A. The Analysis Phase
Weekly Charge = Days per Week * ($0.05 per square feet + $5
per desk)
Of course, contract information can change so the program
should be able to modify the contracts.
This problem description has been kept vague on purpose. Users
rarely can describe their own problems in a way useful to the
programmer. It is our job to tease out the information we need
and hand it back to the user to make sure our analysis is correct.
As a first step then, here is a problem narrative for this problem
Weekly Charge = Days per Week * ($0.05 per square feet + $5
per desk)
This description is the same but more detailed than the original.
Hopefully, enough detail has been provided to help us discover
the classes involved, their properties, behaviors, and relationships.
This is a simple problem so it is likely that we will succeed.
Be aware that a more complex problem will require both a more
detailed problem narrative and a more complex analysis of that
narrative.
B. Part One of the Design Phase - Discovering the Classes
Weekly Charge = Days per Week * ($0.05 per square feet + $5
per desk)
In this example all nouns and verbs have been underlined or italicized
the first time they appear. Since the underlined and italicized
words are to be used as clues in discovering the necessary classes
etc., it is not necessary to 'mark' clues that have already been
discovered. Likewise, words that really refer to the same thing,
as in "office complex" and "office" are not
both 'marked'.
Many of the words here really are irrelevant to the process and
can be weeded out immediately. Our goal is to create a 'model'
of the contract world of Olympia. Since concepts such as 'program'
and 'user' (of the program) are not part of that world, they are
not part of the model and can be ignored.
Perhaps not as obvious, 'Olympia' and 'business' can also be ignored.
The task here is to discover those objects that are part of Olympia's
business world. Olympia herself owns that world. She is not contained
in that world. Likewise, the business world is what is being modeled,
it is not part of itself. Any words in a problem narrative that
name the world being modeled or the 'owner' of that world can
be ignored. It is not that these words could be left out of the
problem narrative. They are necessary to describe the problem
but they are not useful as clues in finding the classes and class
properties.
The nouns that are left include:
Now we need to determine which of these represent classes, which
represent objects, which represent properties of objects, which
represent other elements of the program, and which can be ignored.
Let's start with contract. It certainly is a thing, an object
and it doesn't sound like a property or a description of some
other object. Contracts seem to 'exist' by themselves in the little
world we are modeling. As a matter of fact, there are five of
them which means that there are five instances of contract. This
is a sign that we might have a class. Further, each individual
contract is unique but it is likely that they all have characteristics
in common - another sign that we have a likely candidate for a
class. Let's consider it a class for the moment.
How about 'square footage'. By the way it is written, it is clear
that it is meant to describe a characteristic of a contract -
contracts have a square footage associated with them. Therefore
'square footage' is a property of the contract class. This gives
us more confidence that 'contract' is a likely class - we have
found a property for it. Classes without properties (or behaviors)
are of little use in a program.
Next comes 'number of desks'. Here, perhaps, we cheated in our
initial underlining. 'Number of desks' is most likely a property
of contract just as 'square footage' was. If we had just underlined
the word desk, we would have had to have been a bit more careful.
Desks certainly are objects and therefore the concept of desk
certainly could represent a class in our program. However, in
this program do we care about desks in any other way other than
the number of them. For example, do we care about any of the possible
characteristics of desks? None of the rest of the underlined nouns
refer to desks so the answer seems to be no. When we get to the
italicized verbs we will also see that none of them describe behaviors
of desks so nothing about desks seems to concern us or Olympia
except their number in a contract. Therefore in this program desks
are not objects.
Note that in other programs desks could play a significant role
and need to be modeled as objects. Consider a program which acts
as a tool for doing office designs. In such a program users would
be creating office spaces and placing desks as well as chairs,
tables, portable walls, etc. in those spaces as part of the design
process. Such a program would need to keep track of the location
of each desk, its style, size etc. Desks could be moved around,
deleted, and otherwise modified. Now desks have properties (characteristics)
and behaviors, making them very likely candidates for class status.
Back to our problem: 'number of days' fits in the same category
as 'square footage' and 'number of desks'. 'Per week charge' is
also a property of the contract although we will see later that
there is some discussion about how to represent it. That leaves
three nouns to analyze. 'Information' is really just a collective
word for the various properties we have been discussing. It was
useful in the problem narrative but adds no detail here so can
be ignored. ' Formula' is tricky. Yes, one can say that contracts
have a formula but we will see that the formula here will be represented
as a piece of code that calculates the 'per week charge'. In a
programming context the word formula hints at an action ( a piece
of code) more often than at a property.
Finally, we have 'office complex'. There are a number of office
complexes involved in this program - one for each contract probably.
Yet, as with desks, there do not seem to be any properties (or
behaviors as we will see in a moment) associated with this potential
class. If you think about it, we really aren't interested in office
complexes in this program except that contracts are associated
with them. Thus, this noun seems to be another one that was useful
in the problem narrative but not useful in the program itself.
We wind up with one class (contract) and a small set of properties
for that class. At this point you may be thinking, "You (the
authors) wound up with that, but not me. I could never repeat
this process on my own." You may well be correct. Indeed,
it would be possible and quite easy to come up with a different
and probably invalid set of candidate classes. The class discovery/design
process is an art form and there is no clear set of criteria for
determining when one has come up with the best design. That is
where the team approach comes in. A team is more likely to discover
all the necessary classes, their responsibilities, and their collaborators.
Object oriented analysis and design is not easy. There is no simple
algorithm for it. (If so, we could probably turn the process over
to a computer and forget about it.) The "underline the noun
and circle the verb" approach used here is only to get you
started. You will need to use all your common sense and analytic
skills to arrive at a good design.
This process is so hard that, in practice, it is usually done
in teams - even by experts. The CRC approach, for example, is
specifically meant to be executed in teams. Well managed teams
allow the best ideas of each individual to come through while
filtering out the mistakes. Students often resist working in teams
for any number of reasons (grades, schedules....) but if you want
to be trained for the real world, you need to get used to team
work.
What you also need to do at this point is study the designs of
others who are more experienced and then practice. That is what
much of this course is about. Many of the design exercises in
this course will expect you to work in teams. Try to learn from
your fellow students. In addition, you may well be asked to turn
in your designs before you proceed to the coding stage. This will
help ensure that what you code is based on good design.
VI. Discovering the Responsibilities of the Contract Class
As with the nouns we first need to consider if there are any verbs
that can be easily ignored. Remember that a class responsibility
is a behavior that instances of the class should be capable of
performing. Therefore, what we are looking for here are behaviors
that contracts will be expected to perform. (Again, it might help
if you think of a contract as an actor.)
Problem narratives often include verbs that refer to the person
or organization for whom the program is being written. In this
example, we have "Olympia owns" , "Olympia
wishes", and "she has". These are
not actions of a contract and can be ignored. The remaining verbs
include:
It turns out that many verbs provide very strong hints about the
structure and behavior of classes. As an example of structure
(another way of looking at properties): whenever you see verbs
such as "keep track of", "saves", "stores",
"holds", whose subject noun is a candidate class (such
as contract in this example), the words after those verbs are
likely to be properties of the class. In the program narrative
the first use of 'keeps track of' refers to the program itself
and can be ignored on the same basis that 'owns', wishes' and
'has' were ignored. However, the second usage has 'contract' as
its subject and refers to 'square footage', number of desks' and
'number of days'. We can interpret this to mean that the class
'contract' has the properties 'square footage', number of desks'
and 'number of days'. Here is corroborative evidence that we
are on the right track with our analysis.
Such verbs hint at the characteristics of a class, but they do
not indicate any behavior required of the class. On the other
hand, verbs such as change, set, or modify indicate both possible
properties and the action of changing, setting or modifying the
values of those properties. (They indicate properties because
something can't be changed or modified or set unless it exists
and it is not the object that changes - objects of one type (class)
do not become objects of another type). Therefore, in our example,
we can conclude that instances of class contract must be able
capable of (responsible for) changing the 'information' they hold,
that is, the 'square footage', number of desks' and 'number of
days' since the narrative talks about the values of these properties
changing.
At this point we have the following responsibilities for the 'contract'
class:
We now move on to the verb 'initiate' which has a very specific
meaning in this example and in object oriented design in general.
The idea behind this word is that a contract must be created (started,
initialized, instantiated, constructed) with specific values for
at least some of its properties before it can be used. (For
example, could an actual contract (as opposed to the idea of a
contract) exist without referring to a specific office.) This
idea of creating specific instances with specific attribute values
is true of all objects. A class simply describes a type of object.
Each specific object (instance) belonging to some class must be
explicitly created. Therefore each class must have at least one
function that handles instance creation (initialization). This
will be true even if there is no verb such as initialize, create,
construct, or instantiate in the problem narrative. Such functions
are called constructors.
In our example, we are told that all the information for a contract
must be provided at initialization time. This is a decision made
by Olympia and need not have been the case for contracts and,
as we will see, is often not the case for other classes. To pursue
this for a moment, suppose that we had included the name of the
person who will be signing the contract as part of the information
held in the contract. (This would certainly be part of any real
contract record but is ignored here for simplicity and ease of
programming.) Then one could imagine an initialization function
(constructor) that requires at least the name (how can you have
a contact without that contract involving a specific person or
organization) but that, for the moment, leaves blank some of the
details of the contract.
Another verb that often provides a useful hint is 'get'. It strongly
implies that the class must be capable of reporting the values
of the properties. This does not mean that the values are output
but that they can be retrieved from an instance and used by some
other part of the program. In concrete terms what this means is
that there is a function (after all, functions implement behaviors)
for each property that returns the value of the property. It is
usually considered wise to not have the function output the value
itself because the object does not know how the value is to be
used. Maybe it is not going to be output.
In our example then we need at least three such 'get' functions,
one for, the 'square footage', number of desks' and 'number of
days'. In this material we will use the word 'provide' instead
of 'get' to avoid some confusions students sometimes have. Thus,
the contract class has the following additional responsibilities:
We probably also need a 'get' or 'provide' function for the 'per
week charge' also but this is a special kind of property since
it is a calculated property. When we look at the verb 'calculated'
the question arises should there be a memory location set aside
to hold the 'per week charge' or should it be calculated every
time it is needed? If it is calculated every time it is requested
then the program must do the math over and over even if none of
the values upon which it depends have changed. Why do that work
over and over? But, suppose we do set aside a memory location
to hold the 'per week charge' and do the calculation once. Now,
suppose that later the number of desks in the office changes.
Since this is one of the properties upon which the 'per week charge'
depends, any change in it forces a change in the per week charge.
How do we make sure that the 'per week charge' is again calculated?
It turns out that although one can write code to accomplish this,
it is often easier to simply have an instance redo the calculation
whenever the value is asked for. The conclusion, therefore, is
that the contract class will have a 'Provide Per Week Charge'
function but there will not be a 'weekly charge' property.
There are three verbs left:
None of them really involve behaviors of contracts. 'Provided'
refers to the need of the program ( and ultimately the user of
the program 'to provide' information while 'to be cleaned' certainly
is not a behavior of a contract. 'Involves' implies a relationship.
Is it possible that we have skipped a class or classes - classes
that would be responsible for providing and cleaning or that
would be 'involved' with the contract class? This possibility
should not be overlooked - one part of the design process can
often help in some other aspect of design. However, in this
case there is nothing to worry about. First, the problem narrative
says that "contracts involve office complexes" but we
have already carefully analyzed the noun office complex and decided
that it did not represent a class that would be useful in this
program. Second, the other two verbs really refer to the user
of the program - the user who would be 'providing' the information
and the user or an associate who would be doing the 'cleaning'.
In our analysis we also decided that 'user' was not a useful
class concept in our program.
To summarize: Our analysis shows that we need one class, call
it contract, that has eight behaviors or responsibilities:
Since there is only one class we do not need to worry about collaborators.
Here then is the CRC card for the class 'contract'
VII. Classes as Types
Classes can be seen as complex types - they certainly name some
category or classification. A class then is nothing but an idea,
a concept, a description. In a program a class only represents
the possibility of a certain type of object. Only instances of
a class can be used/manipulated in a program. The process is that
a programmer first declares a class (describes the properties
and behaviors of the class). He or she then defines the behaviors
of the class (remember the difference between declaring and defining
a function). And finally, he or she declares instances of the
class.
The interesting thing about a class (or type) is that is that
one can imagine a class that does not have any actually existing
members. Most of us can imagine the class "hairy monsters
with three heads that live on the far side of the moon".
It is doubtful, however, if instances of such a class exist.
VIII. Some Additional Ideas on Discovering the Responsibilities
of a Class
Such interface functions are often not 'discovered' until
one actually begins coding. That is perfectly OK - a good design
is meant to give the programmer a plan to follow and plans can
be modified along the way. However, it is best to determine as
much as you can at design time so you should ask the following
questions of each class:
IX. Class Declarations
Therefore, when we write functions we are extending the instruction
set of the language! We need to be able to do the same with types.
In the last section we introduced the idea that a 'class' is a
type. In this section we explore this idea further and study one
way to extend the set of types built into C++ - by declaring classes.
To make our discussion concrete we will continue with the class
'contract'.
To declare a new class one starts with the word 'class' followed
by the class's name and a set of curly brackets to indicate that
all that follows relates to the named class. Oh, and don't forget
the semicolon at the end - when forgotten, some strange compiler
errors can result.
The first thing to understand then is that classes have public
and private parts. Those elements of a class that are
to be encapsulated in the class are declared in the private part.
It is this 'private' declaration that causes the organization
or structure of encapsulated data members to be hidden or 'unavailable'
outside the object.
In almost all cases the data members are private to a class.
Access is only through a set of member functions (the interface)
whose specific tasks are to provide or change the values of the
data members of a class. To make these functions accessible they
must be included (declared) in the public part of a class. In
other words, any member function that represents a responsibility
of the class/object or has the sole purpose of allowing access
from outside the object to the member data of an object must be
declared in the public part of the class. Such functions are called
interface functions and you should make sure you include
all necessary interface functions in your class design.
All this means that class declarations are complex and one might
want to ask again why have encapsulation! After all, shouldn't
I as a programmer be able to change the value of any variable/data
member that I want to from anywhere in a program ? From one perspective
the answer to this question might well be 'yes' - if programmers
were perfect and never made mistakes. Unfortunately, they aren't
and encapsulation helps avoid mistakes. It also both allows programs
to be more easily modified and supports team programming. A programmer
can quickly devise a class, complete with the necessary interface
functions and give it to others to use. The programmer can then
go back and improve on the class while the other team members
write code based on the class interface. As long as the programmer's
improvements do not change the interface, the modifications have
NO effect on the code written by those using the class. If the
other team members had direct access to the insides of the class
and took advantage of the internal structure, any changes made
to that structure would require changes in other team member's
code.
This is the same way that a car works. One can have the engine,
the carburetor, the exhaust system etc. changed and still not
need to re-learn how to drive since the interface (steering wheel,
pedals ...) are all the same.
Back to the design: member functions also can be private. Clearly
the interface functions must be public since they must be accessible
to any code that wants to manipulate an instance/object of a class.
Private functions are those functions that are known only by the
objects of a class and therefore they cannot be used outside the
objects. Usually such functions exist because the programmer decides
that some member function is to complex or has elements that can
be re-used in other member functions. He or she then decomposes
the member function as in chapter four. It is these sub-functions
that are inaccessible outside the object and therefore are private.
Since classes have public and private parts, the code for our
'contract' class must reflect this:
The words 'public' and 'private' could be interchanged but most
experienced programmers prefer the order we have chosen. This
encourages the focus to be placed on the public part of the class
as opposed to the private part. Since the public part of the class
comes first and the member functions are public, let's first focus
on designing and declaring them. Here is where we really use the
material from the earlier chapters so make sure you are comfortable
with function design, declaration and definition.
The member functions we have determined need to be included in
the class 'contract' are:
B. Declaring Constructors
That takes care of the input-output design issue. Now for the
receives and returns. Special rules exist for constructors in
C++. First, any constructor for a class must have the same name
as the class. Therefore, the constructor for the class 'contract'
must be called contract. Second, constructors, by definition,
do NOT return anything and should not have the word 'void' in
front of them. This means we can ignore the second of the two
questions usually asked of functions. The first is still relevant:
For all constructors the answer takes the same form - what is
needed are the values for whatever properties need to be set at
the moment the instance is created. This information is provided
to us in the problem narrative. It says that the values for the
properties 'square footage, 'number of desks, and 'number of days'
must be provided. So, that is the data that must be passed to
the constructor for the class' contract'. Later we will see slightly
more complex versions of this answer, especially involving the
idea of default values - values that are assumed to be
valid unless the class is told otherwise. For instance, if almost
all office cleaning contracts where for five days (Monday ...
Friday) we could have five as a default value for 'number of days'
and pass in a different value only when needed. (You do not know
how to do that yet but you will learn!). In any case, determining
what a constructor needs as 'receives' always comes down to determining
what properties must be initialized and how.
Here is the design for the constructor:
Inputs: NONE
Receives: Square Footage, Number of Desks, Number of Days (all
Integers)
It is rare that the algorithm for a constructor is anything more
complex than a series of assignment statements involving the properties
being initialized so you will not be required to explicitly write
an algorithm for the constructor, Be sure, however, that you understand
what is supposed to happen and review carefully the definition
of the constructor below.
The declaration of the constructor (inside the declaration of
the class)looks like this:
C. Declaring Functions that Change the Values of Properties
ChangeSquareFootage
Inputs: NONE
Receives The New Square Footage (an integer)
The design for the other two 'change property' functions are exactly
the same and here we have another pattern. All member functions
whose purpose is to change the value of some property of a class
have the same design. They also have the same declaration form
as is shown below:
First, the function name gives away the purpose and goal of the
function. And, again, there are no inputs or outputs - based on
the same arguments we used for the 'Change ...' functions. Now,
however, the receives and return analysis is backwards. These
'provide ...' functions do not need anything from the calling
function in the same way that a function that gets a value from
the user does not have any 'receives'. Member functions have access
to all the member data in the instance of the class they are associated
with. Therefore, 'ProvideSquareFootage', for example, has access
to the square footage of any instance it is associated with. (The
meaning of 'associated with' must be kept vague for the moment.
When we discuss how to use these functions, this will be come
clearer.)
With regards to the return analysis, just like a function that
gets a value from a user, these functions return one value - the
value found in the property they are responsible for. Here then
is the design for the first of these functions and the declarations
for all three.
It should also be observed that the decision to always calculate
or not is irrelevant to any function that uses this function.
All that matters to any function that needs the per week charge
is that the charge is delivered correctly. Therefore, we could
later decide to change our mind and have a class property that
holds the per week charge and carefully update it when needed.
Nothing in the rest of the code would need to change. Here again
is an example of the power of encapsulation and function decomposition.
Changes to one part of a program can be made with little or no
effect on the rest of the program.
So, now we have a function called 'ProvidePerWeekCharge' whose
purpose is not quire specified by its name. Usually we want a
name that indicates exactly what the function does but in this
case we have chosen a name that indicates to the user some of
what happens while not telling the whole story - and that is OK!
In the Purpose and Goal statements, however, we to want to state
clearly what the function does:
ProvidePerWeekCharge
As with all functions that do not indicate any inputs or outputs
in the purpose, this function does not have any inputs or outputs.
We proceed then to the 'questions'. First, "what does this
function need to accomplish its task?" The answer to that
is found in the formula:
Clearly, the calculation cannot be accomplished unless the function
has the Square Footage, the Number of Days Per Week, and the Number
of Desks. However, where does it get this information from? To
answer this review where 'ProvideSquareFootage' gets the square
footage from. The key here is that all member functions have access
to all member data of the instances they are associated with.
Thus, the member function 'ProvidePerWeekCharge' has access to
the square footage, number of desks, and number of days data for
whichever instance the function is associated with. The conclusion
then is that 'ProvidePerWeekCharge' has no 'receives' because
it has access to all the information it needs.
Now, what are the returns? If someone asked you to perform some
calculation, what do you think they would want back? The result
of the calculation, of course! And, that is what this function
will return, the calculated weekly charge. Since a 'charge' is
a dollar amount, the result is of type double. The design and
declaration for this function should be inserted just after the
declaration for 'ProvideNumberOfDays' in the declaration for the
class 'contract'.
Inputs: NONE
Receives: NONE
*/
This completes the declaration of all the member functions for
this class and all the elements of the public part of the class.
E. Declaring Member Data - the elements of the Private Part
of a Class
We have seen how local variables have access restricted to the
function they are local to. Member data are variables with access
restricted to the instance they belong to. When we declare the
member data (and the member functions) of a class we are describing
in general terms the properties and behaviors of instances of
the class. However, we are NOT creating any specific instances
- just as when we describe how "monsters with three heads
that live on the far side of the moon" might look and behave,
we are not actually creating any such monster. It is the constructor
that creates instances and we will see in a bit how to use the
constructor.
The declaration for data members is exactly the same as for any
other variable declaration. It should be noted that this time
we are dealing with pure declarations and not declarations combined
with definitions since memory is not set aside for the data members
until an instance is created. Also, note that we have as many
memory locations set aside for a certain property declared in
a class as there are instances of the given class. In other words,
if there are ten contracts then there are ten versions of each
of the data members and therefore, for example, there are ten
memory locations for square footage .
Since our specifications and design have pretty much determined
the type for all the data members, the actual declarations are
easy. Here is the completed declaration for the class 'contract'.
That finishes the analysis and design phases. It is now time to
focus on the how. If we have done our work well, it should be
easy to define each member function. In the next section we will
test this theory and look at the specifics of member function
definitions.
X. Defining a Class
With classes the separations are usually more distinct. It is
common practice to put the class declaration in one file, the
class definition in a second file, and the program that uses the
class in a third file. If there is more than one class in a program,
each class gets its own declaration file and definition file.
(Some programmers combine a number of class declarations in one
file but the reasons for that are usually based on issues we have
not discussed here.)
All the code we have written so far in this chapter should be
in one file. If you recall from our earlier work, all files should
have at least two comment lines at the top providing the name
of the file and a brief description of what the file is about.
By convention files that declare classes use the file extension
'.h' - for 'header' file. We have already used one header file
in the code in earlier chapters - "iostream.h". That
file was provided by the compiler distributors and we included
in by using the "#include" directive followed by the
file name surrounded by angle brackets, <...>. The header
file for the 'contract' class will also be included via the "#include"
directive but the file name will be placed in double quotes, "...",
to indicate that it is a programmer-defined header file.
In the case of the code we just wrote we should have added two
lines at top similar to:
// Declaration of the class Contract
Similarly, for the file to hold the class definitions, we begin
with two comment lines:
// Definitions for the class Contract
Note that the file has the same name except that the extension
is different. We use the .cpp extension because this file contains
instructions for the individual functions. Since the specifications
for the problem go in the file containing 'main' they do not appear
here or in the '.h' file. From what we have learned already, what
comes next is the line or lines to 'include' any necessary files.
In our analysis above we decided to not have any of the functions
provide for input or output. For that reason, we do not need to
have the line:
in our code. However, there is one header file we do need to include.
Our definitions for the class 'contract' are going to use the
various elements of the 'contract' declaration and, therefore,
that information needs to be available in this file. To make it
available we need to include the header file for the contract
class - the file we called 'contract.h' - using the code:
You can think of the 'include' directive as telling the
computer to copy in the code found in the file 'contract.h'.
You might ask then why we don't just have one file that has the
declarations and the definitions as we did in our previous programs.
For simple programs such as the one we are presently working on
we could do exactly that. However, this would not be wise in more
complex programming situations. First, we have seen the importance
of breaking our work up into pieces. For programs of thousands,
hundreds of thousands, even millions of lines of code, having
one file for the whole program would simply be unworkable. Second,
we would like to have our code be re-usable. If someone writes
a piece of code that provides specific functionality, it would
be smart to include that code in any program that requires that
functionality without including unnecessary additional code. Indeed,
such re-usability is supposed to be a key feature of the object
oriented approach. Once a class is declared and defined, it should
be possible to use the code for that class in any programming
problem requiring that class.
Third, when we break a large program up into separate files, the
code in some of those files at least may need to know about a
specific class. Thus, we may need to include the declaration code
for that class in those files. We would not want, however, to
include the class definitions in each file because, if we did
so, the final, combined program would have more than one definition
for the same member functions. This is illegal.
The final answer to the question at this point might best be,
"Trust the experience of professional programmers."
Given that, let's proceed. We will start by defining our constructor.
The function members of a class are closely associated with a
class. So, closely that we need to tell the system that the function
we are about to declare belongs to the class 'contract'. Here
then is the first line of the definition for the constructor 'Contract':
The 'Contract::' at the front of this line tells the compiler
that what follows is a function definition for a member function
of the class 'contract'. The fact that the function being defined
has the same name as the class, indicates to the compiler that
it is a constructor for the class. Without the 'contract::' the
function 'contract' would not be seen by the compiler as associated
with any class at all and certainly would not be seen as a constructor.
It would be considered the same as the function definitions we
have already seen.
The rest of the code for the constructor is very simple. As we
have already discussed, the purpose of the constructor is to create
an instance and assign values to all or some of an instance's
data members. According to our analysis, all the properties need
initialization - that is the reason we have three 'receives' in
the declaration. The code now uses those receives.
Contract::Contract(int sqFootage, int numDesks, int numDays)
One mistake that beginners often make is giving the same name
to a parameter (a receive) that they give to a data member. Note
that in this code 'squareFootage' is the symbolic name of the
memory location holding the number of square feet involved in
some specific contract, while 'sqFootage' is the symbolic name
for the memory location used to pass the information to the constructor.
The value in 'sqFootage' will be assigned to the memory location
symbolized by 'squareFootage'. If both symbolic names were the
same, how would the compiler know which memory location was to
have its value assigned to which other memory location?
The code for most of the constructors you will work with in this
course will be as simple as this one - essentially some set of
assignment statements. One other detail that should be noted -
because it sometimes catches students - is that there is no semicolon
at the end of the function definition.
We move on then to the other function members. It turns out that
there is little new here other than the need for classname::
before each function definition. The 'change' functions all
are 'one-liners', for example:
void Contract::ChangeSquareFootage(int sqFootage)
The same is true of all but one of the 'provide' functions, for
example:
A few points about this member function definition:
Here then is the completed class definition:
A. Declaring the Instances
The design process has turned up one class with a number of basic
functions and properties. A program that gave 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.
The best 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) each instance will be instantiated with. Those same
values should be output as a result of step 1. 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
processing of the instructions.
We have five instances to create so we need five sets of property
values. 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:
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 but the program will output
the values of the data members of the five instances. Therefore,
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
VII
) 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 almost the same syntax as we used to declare an 'instance'
of type double or int or ....
Remember that we said that 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. If we rewrite the line as :
the memory location symbolized by this variable contains the value
2.1
If we were to write the line:
we would be 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. However,
this is the act of creating, instantiating, or constructing the
instance and the constructor for the class 'contract' requires
values for the three properties, 'square footage', 'number of
days' , and 'number of desks'.
Now you see why the constructor for a class has the same name
as the class itself. The word 'Contract' (upper case 'C') in the
line of code above is both a type name AND a very strange 'call'
to the constructor for the class. Since it is a 'call', we need
to provide the three 'receive' parameters. Here is how the line
should look:
The numbers inside the parenthesis come from the table of initial
values we just finished working on. What this then tells the system
to do is:
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()
B. 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 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, to use these functions we can't just write, for example:
as we might have done with regular functions in chapter 4. Member
functions always work with specific instances of a class but 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'
- a logical choice. 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!
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.
C. Using the Change.... Member Functions
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 instructions except that this time the functions have one
parameter and return nothing. 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 also 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, "common.h", "contract.h", "contract.cpp", and "ch5tst1.cpp"
C. Putting It All Together
We have already seen how to accomplish this with the header file
via the include statement. As we said, the "#include
"
statement copies the code in the included file into to file with
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
- the definition for the functions declared here is available;
just wait until 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 can be found in the document "BorlandProject.htm" )
Click here for the completed driver program. Follow the instructions for your version of C++ to compile and link this program with the .cpp file containing the definitions for the class member functions.
|