Lab 10: Building Classes
Introduction
Most of the problems we have examined have had relatively
simple solutions, because the data objects in the problem could be
represented using the predefined C++ types.
For example, we can represent a menu with a string,
a choice from that menu with a char,
the radius of a circle with a double, and so on.
The problem is that real-world problems often involve data objects
that cannot be directly represented using the predefined C++ types.
For example, suppose that we know a certain gourmet chef named PiËrre
whose recipes are written to make 12 servings. The difficulty is
-
PiËrre frequently must prepare a dish for numbers of customers
different than 12
(e.g., 1 customer = 1/12 of a recipe, 2 customers = 1/6 of a recipe,
15 customers = 15/12 = 5/4, and so on.)
-
PiËrre's recipes are written using fractions
(e.g., 1/2 tsp., 3/4 cup, etc.) so that he must
multiply fractions to reduce the size of a recipe, and
-
PiËrre is so poor at multiplying fractions,
that he has hired us to write a program that will enable
him to conveniently multiply two fractions.
We have provided two programs for today's exercise: pierre1.cpp
and pierre2.cpp.
In the first part of the exercise, we will do what is needed to
get pierre1.cpp operational,
and in the second part of the exercise, we will extend the first part
so that pierre2.cpp is operational.
Getting Started
Begin by making a new directory in which to store your work for this exercise.
Then save copies of the files
pierre1.cpp,
pierre2.cpp,
Fraction.h,
Fraction.cpp,
Fraction.doc, and
Makefile10
in this directory.
Take a moment to compare the programs in pierre1.cpp and
pierre2.cpp.
Each program implements the same basic algorithm:
1. Get oldMeasure, the fractional measure to be converted.
2. Get scaleFactor, the fractional conversion factor.
3. Compute newMeasure = oldMeasure * scaleFactor.
4. Output newMeasure.
Note that a solution to PiËrre's problem is quite simple,
given the ability to define, input, multiply and output fraction objects.
The two programs differ only in how they perform input and output
Fraction objects.
Note also that some lines are "commented out" at present.
We will be "uncommenting" these lines as we develop the functionality
needed in order for such lines to work properly.
The difficulty is that there is no predefined C++ type Fraction
by which such objects can be defined or operated upon.
In such situations, C++ provides a mechanism by which a programmer
can create a new type and its operations.
This mechanism is called the class,
which is the subject of today's exercise.
Creating Classes
In C++, a new type can be created by
-
Defining the data objects that make up the
attributes of an object of the new type
(i.e., the things of which an object of that type consists); and
-
Surrounding those definitions with a class structure,
which has the form:
class TypeName
{
public:
private:
};
where TypeName is a name describing the new type.
As indicated, a class has two sections,
a public section and a private section.
The public section is where class operations are declared,
and the private section is where class attributes are declared.
To illustrate, suppose that we want to define a new type whose objects
can be used to store Cartesian coordinates.
Such an object has two attributes: an X-value, and a Y-value, both reals.
We can define data objects for these attributes as follows:
double myX,
myY;
(To distinguish attribute identifiers from other identifiers,
we will place the word my at the beginning of a data member's name.)
We then surround them with an appropriately-named class structure:
class Coordinate
{
public:
private:
double myX,
myY;
};
The result is a new type, named Coordinate,
which can be used to declare data objects, such as a point:
Coordinate point;
The object point then has two real components,
one named myX and the other named myY.
In general, the data portion of a class definition can be thought of
as following this pattern:
class TypeName
{
public:
private:
Type1 AttributeList1 ;
Type2 AttributeList2;
...
TypeN AttributeListN ;
};
where each Typei is any defined type;
and each AttributeListi is a list of the
Typei attributes of an object of type
TypeName.
Now, if we apply this approach to the problem we are trying to solve,
we see that we need to identify the attributes of a Fraction object.
If we examine a few fractions
1/2 4/3 4/16 16/4
we can see that each fraction has the form:
number1/number2
where number1 is called the numerator and
number2 is called the denominator.
The numerator and denominator are different from one fraction to another,
and so these quantities must be recorded for any given fraction value;
however the / symbol is common to all fractions, and so it
need not be recorded as an attribute.
A fraction thus has two attributes, its numerator and its denominator,
both of which are integers.
Begin editing the file Fraction.h,
and define two integer data objects named myNumerator and
myDenominator to represent these two attributes.
(Yes, you should prepend my to the beginning of each name,
for reasons that will be apparent shortly).
Then surround these definitions with a class structure that declares
the name Fraction as a new type whose objects contain these
two attributes.
Be sure to arrange your class so that myNumerator
and myDenominator are within the class,
but following the keyword private:.
Given this declaration of Fraction,
object declarations like this
Fraction oldMeasure;
...
Fraction scaleFactor;
can be thought of as defining two data objects with the following forms:
The objects within a class object that store its attributes
are usually called the data members of that object.
Note that each object of type Fraction has its own copy of
each of the attributes we defined.
This is why we preface the names of these attributes with my,
to indicate that from the perspective of the object,
attributes are characteristics of that object.
That is, within the Fraction object
named oldMeasure,
myNumerator refers to its first data member
myDenominator refers to its second data member.
However, within the object named scaleFactor,
myNumerator refers to its first data member
myDenominator refers to its second data member.
Each object thus has its own separate data members.
Since a class may contain an arbitrary number of attributes,
a software model can be constructed for virtually any real-world object,
simply by defining data objects for each of its attributes,
and then surrounding those data objects with an appropriately-named
class structure.
In the source program pierre1.cpp,
the definition of oldMeasure
is currently commented out.
Modify your source program so that this declaration is no longer
commented out (but the subsequent lines are).
Then compile your source program, to test the syntax of what you have written.
When it is correct, continue to the next part of the exercise.
Function Members
Besides having data members, a class can also have functions
which are called the function members (or member functions)
of the class.
Function members provide a means by which the operations
on a class object can be encoded.
Function members must be prototyped within the class structure itself;
and are normally defined in the class implemention file.
However, very simple function members (i.e., that fit on a single line)
are by convention defined in the header file,
following the class declaration, and prefixed by the keyword inline.
(If we designate a function as inline, we are suggesting to the
compiler that it replace calls to the function with the actual
body of the function, substituting arguments for parameters as appropriate.
This should only be done in the header file, not in the implementation file.)
Class Structure and Information Hiding
One of the characteristics of a class is that its data members
are kept private, meaning that a program using the class
is not permitted to directly access them.
(This is a good idea because a program that directly accesses the
data members of a class becomes dependent on those particular data members.
If those data members are changed (which is not uncommon in class
maintenance), then such programs must also be changed,
increasing the cost of software maintenance.)
While it is a good idea for the data members of a class to be kept private,
we want users of the class to be able to perform operations on class objects.
As a result, the operations should be declared as public members,
in contrast to the data members.
This is the reason for the keywords public: and private:
within the class: the space between public: and private:
defines a public section where function members can be declared,
and the space between private: and the end of the class
defines a private section where the data members can be declared.
The pattern is thus as follows:
class TypeName
{
public:
// Class Operation Declarations - public!
private:
// Class Attribute Declarations - private!
};
All of the declarations that follow the keyword public:
can be accessed by programs using the class; and
all of the declarations that follow the keyword private:
cannot be accessed by programs using the class.
By convention, the "public section" of a class comes first,
so that a user of the class can quickly see what operations it provides.
It is good style to have one "public part" and one "private part" in a class;
however the keywords public: (and private:) can appear
an arbitrary number of times in a class declaration,
if multiple public sections are needed.
Function Members As Messages
We have seen that function members are called differently from "normal"
functions: if two string objects named greeting and
closing are defined as follows:
string greeting = "hello",
closing = "goodbye";
then the expression
greeting.size()
returns the value 5, while the expression
closing.size()
returns the value 7.
Object-oriented programmers like to think of a call to a function member as
a message to which the object responds.
To illustrate, when we send the size() message to greeting,
greeting responds with the number of characters it contains (5);
and if we send the same size() message to closing,
closing responds with the number of characters it contains (7).
The effect of this approach is to shift the point of view from the function
to the object.
Defining a function member is thus a bit different from defining a "normal"
function, because the definition of a function member describes what
a class object does when it receives that message.
Put differently, function members are usually written from the perspective
of the class object.
Part I. pierre1.cpp
In this first part of today's exercise, we will focus on adding the
function members to class Fraction needed to get
pierre1.cpp operational.
An Output Function
To facilitate debugging a class, it is often a good idea to begin with
a function member that can be used to display class objects --
an output function.
From the perspective of our Coordinate class,
we can specify the task of such a function as follows:
Receive: out, an ostream to which I am to write my values.
Output: myX and myY, as a pair.
Passback: out, containing the output values.
Function members must be prototyped within the class declaration,
so if we were to name our function Print(), we would write:
class Coordinate
{
public:
void Print(ostream & out) const;
private:
double myX,
myY;
};
Function members that do not modify the class data members
are declared as const function members,
by placing the keyword const after the function's
parameter list, as shown.
As it happens, this is a fairly simple function,
so we would define it within the header file Coordinate.h,
following the declaration of class Coordinate,
and precede its definition with the keyword inline.
To define Print(), we must inform the compiler that
this is a function member, as opposed to a "normal" function.
This is done by preceding the name of the function with
(i) the name of the class of which the function is a member; and
(ii) the scope operator (::).
That is, we would define Print() as a function member
of our Coordinate class as follows:
inline void Coordinate::Print(ostream & out) const
{
out << '(' << myX << ',' << myY << ')';
}
As explained earlier
-
inline suggests to the compiler
that calls such a simple function should be replaced with the
body of the function;
-
void tells the compiler that this function returns nothing;
-
Coordinate:: tells the compiler that this function
is a member of class Coordinate;
-
Print is the name of the function;
-
(ostream & out) is the parameter list of the function; and
-
const tells the compiler that this function should not
modify any of the data members of class Coordinate.
Note that const must be present in both the prototype
and the definition of a function member that does not modify
its function members.
Note also that the prefix my helps to reinforce the notion
that this is a message to which a Coordinate object responds.
That is, if point is a Coordinate object whose
X-value is 3 and whose Y-value is 4,
then the statement
point.Print(cout);
displays via cout:
(3,4)
Similarly ,if origin is a Coordinate object whose
X-value is 0 and whose Y-value is 0,
then the statement
origin.Print(cerr);
displays via cerr:
(0,0)
Note that as a message to an object,
a function member must be invoked using dot notation,
which specifies the object to which the message is being sent.
Note finally that as a message to which an object responds,
a function member can directly access the private data
members of the object.
(If we were to omit the Coordinate:: in the definition,
a compilation error would result.)
Using this information as a pattern, prototype and define
a similar Print() function for your Fraction class,
such that if oldMeasure is a Fraction whose
numerator is 3 and whose denominator is 4, then a message:
oldMeasure.Print(cout);
will display
3/4
Prototype this function in the public section of class Fraction,
and define it as an inline function following the
declaration of class Fraction, in the Fraction.h.
Check the syntax of your function, and continue when it is correct.
The Class Constructor
An output operation for a class is of little use unless we are able to define
and initialize objects of that class.
The action of defining and initializing an object is called
constructing that object.
To allow the designer of a class to control the construction of class objects,
C++ allows us to define a function called a class constructor function
that specifies exactly what actions are to be taken when a class object
is constructed.
When a class object is defined, the C++ compiler calls this function
to initialize the object's data members.
For example, suppose we would like for a definition of a
Coordinate object:
Coordinate point;
to initialize the data members of point to zeros.
We might specify this task as follows:
Postcondition: myX == 0.0 && myY == 0.0.
Note that a constructor function does not return anything to its caller,
it simply initializes the data members of an object when
that object is defined.
We specify this behavior through a boolean expression that is
true when the function terminates.
Such an expression is called a postcondition,
since it is a condition that holds when execution reaches the
end of the function.
In order for a function to be a function member of a class,
its prototype must appear within the class, and
the name of a constructor function is always the name of the class,
so we declare this function in the public section of class
Coordinate, as follows:
class Coordinate
{
public:
Coordinate();
void Print(ostream & out) const;
private:
double myX,
myY;
};
Note that unlike our Print() function,
a constructor initializes (i.e., modifies) the
data members of a class.
As a result, it must not be prototyped
or defined as a const function member.
As we said earlier, a function member definition must have
the name of the function preceded by the name of the class
and the scope operator (::),
and simple definitions should be placed in the class header file,
designated as inline functions.
To define a Coordinate constructor function, we thus
write this funny-looking definition:
inline Coordinate::Coordinate()
{
myX = 0.0;
myY = 0.0;
}
The first Coordinate is the name of the class,
telling the compiler that this is a function member of class
Coordinate.
The second Coordinate is the name of the function,
telling the compiler that this is a class constructor function
for class Coordinate.
Given this function member,
when a Coordinate object is defined,
the C++ compiler will automatically call this function
to initialize this object,
which sets the object's myX and myY members to zero values.
Note that a constructor has no return type (not even void).
As was mentioned earlier,
this is because a constructor never returns anything to its caller --
it merely initializes objects of its class.
The pattern for a constructor function definition is thus:
ClassName::ClassName(ParameterList)
{
StatementList
}
where the first ClassName refers to the name of the class,
the second ClassName names the constructor function,
and StatementList is a sequence of statements that
initialize the data members of the class.
Constructors can take parameters, which are defined as they would be
for any other function, and any valid C++ statement can appear
in the body of such a function.
Using this information, prototype and define a constructor for your
Fraction class, that satisifies the following specification:
Postcondition: myNumerator == 0 && myDenominator == 1.
That is, the definition:
Fraction oldMeasure;
should initialize the data members of oldMeasure appropriately
to represent the fraction 0/1.
Store the prototype in the public section of class Fraction,
and define it as inline, following the declaration of class
Fraction, in Fraction.h.
Test the syntax of what you have written, and continue when it is correct.
|
|
|
|
Home
Help
Lab 0
Lab 1
Lab 2
Lab 3a
Lab 3b
Lab 4
Lab 5
Lab 6
Lab 7
Lab 8
Lab 9
Lab 10
Lab 11
Lab 12
Lab 13
Membership
Login |