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. 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 just a single, predefined C++ type.
Let's consider two different problems.
Problem #1: Cartesian points. In the algebra courses you've taken, you have probably graphed some functions on a two-dimensional graph. These graphs typically use a Cartesian coordinate system, with x- and y-coordinates, to plot the points.
This graph plots the point (7,3), which is 7 units to the right along the x-axis and 3 units up along the y-axis. In this lab, we'll only worry about representing a single point. This work, though, could easily be used in a program to generate some actual graphs in a Cartesian coordinate system.
Problem #2: Fractions. The other problem is doing fractional arithmetic. Suppose that we know a certain gourmet chef named Pièrre whose recipes are written to make 12 servings. There a few difficulties:
We will work through two programs to manipulate fractions.
Coding. Keep in mind that you will write the code
for the Fraction
class. The Coordinate
class is
presented here as an example to help you write code for the Fraction
class.
Directory: lab11
Fraction.h
, Fraction.cpp
, and
Fraction.doc
implement the Fraction
class.gcd.h
and gcd.cpp
implement
Euclid's greatest-common-divisor algorithm (used by the Fraction
class).pierre1.cpp
and pierre2.cpp
are
drivers for the Fraction
class.Makefile
is a makefile.gcc
users need a makefile; all others
should create a project and add all of the .cpp
files to
it.
Add your name, date, and purpose to the opening documentation of the code and documentation files; if you're modifying and adding to the code written by someone else, add your data as part of the file's modification history.
Take a moment to compare the programs in pierre1.cpp
and
pierre2.cpp
. Both programs implement the same basic algorithm:
oldMeasure
, the fractional measure to be converted.scaleFactor
, the fractional conversion factor.newMeasure
= oldMeasure
* scaleFactor
.newMeasure
.A solution to Pièrre's problem is quite simple, if we have the ability to define, input, multiply and output fraction objects. That's the work we'll be doing in this lab.
The two programs differ only in how they read and write fraction objects. The second version has a more familiar look to it, but will require more work from us. So the first version will be useful for getting something workable, but it's missing some input and output features.
The difficulty is that there is no predefined C++ type Fraction
. In such very common situations, C++ provides a
mechanism by which a programmer can create a new type and its
operations. This mechanism is called a class.
In C++, a new type can be created by
The class structure mentioned in this second step looks like this:
class TypeName
{
public:
private:
};
where TypeName
is a name describing the new type. A class
is (almost) always defined in a header file.
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.
Let's first consider a point in Cartesian coordinates. A point consists of an x-coordinate and a y-coordinate. We can easily write two variables to hold these two values:
double myX, myY;
Of course, the context for this declaration is all important. We declare these variables in an appropriately-named class structure, making them instance variables:
class Coordinate { public: private: double myX, myY; };
We use the prefix "my
" at the beginning of a name for an
instance variable name to encourage an internal
perspective. We imagine ourselves as the object we're trying to
describe. For example, when figuring out these instance variables, I
could say, "I am a Cartesian coordinate, and I have an x-coordinate
and a y-coordinate." Since they are my attributes, I label
them as such. This way I shouldn't get them confused with other
objects that I work with.
The result is a new type, named Coordinate
, which can be used
as a type for new objects:
Coordinate point1, point2, point3, point4;
Each point
has two I
double
components,
one named myX
and the other named myY
. This is why
myX
and myY
are known as instance variables: every
instance of a Coordinate
has its own copies of these
variables. The neat thing is that we only had to declare myX
and myY
once in the class definition.
We can now expand our general pattern for a class definition:
classwhere eachTypeName
{ public: private:Type1
InstVarList1
;Type2
InstVarList2
; ...TypeN
InstVarListN
; };
TypeI
is any defined type and each
InstVarListI
is a list of instance variables of
type TypeI
.
Now let's apply this same thinking to the fraction problem. We first
need to identify the attributes of a Fraction
object. Here
are a few fractions:
1/2 4/3 4/16 16/4Each fraction has the form:
wherenumber1
/number2
number1
is called the numerator
and number2
is called the denominator.
A fraction needs both a numerator and a denominator. However, the
/
symbol is common to all fractions, and so it is not
recorded as an attribute of a fraction. Consequently, a fraction has
just two attributes, a numerator and a denominator, both of which are
integers.
Edit the file Fraction.h
:
Fraction
.myNumerator
and myDenominator
.Given this declaration of Fraction
, we can make the following
declarations:
Fraction oldMeasure; ... Fraction scaleFactor;These declarations define two objects with the following forms:
Again, note that each instance of type Fraction
has its own
copy of each of the attributes we defined; that's why they're called
instance variables. It's also why we prefix the variable name
with "my
", to encourage an internal perspective.
In the source program pierre1.cpp
, uncomment the definition
of oldMeasure
. (Do not uncomment anything else, yet.)
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.
Besides having instance variables, a class can also have methods. A method is a function declared inside a class to provide an operation for objects of the class.
We've used several methods already. A vector
has a size()
method. A string
has a substr()
method. But
until now we've only used methods. Now we get to define our
own methods in our own classes.
Methods are prototyped within the class structure itself (in the
header file), and they are normally defined in the class
implementation file. However, very simple methods (i.e., ones that
fit on a single line) are defined "inline": in the header file, following the
class declaration, prefixed by the keyword inline
. The
reasons for this are a bit technical and beyond the scope of what
we're really trying to do. Just remember to define simple methods in
the header file with an inline
in front of them.
One of the characteristics of a class is that its instance variables
are kept private, meaning that a
program using the class is not permitted to directly access them.
Mostly this is an issue of trust. In the case of a Coordinate
object, it probably wouldn't be too bad if other
programmers had direct access to the myX
and myY
instance variables. However, in the Fraction
class, we must
make sure that myDenominator
does not become zero since
division by 0 is undefined. If we hide away myDenominator
from the casual programmer, we can write the methods of the Fraction
class so that myDenominator
never becomes 0.
In practice, it is not worth the time and effort to try to figure out which instance variables are safe to let everyone access directly. Common practice is to make all instance variables private.
On the other hand, we do want users of the class to be able to
access the operations of a class. As a result, the operations should
be declared in the public section
of the class. Anything defined in the public section can be accessed
through an instance of the class from any part of the program. So,
wherever we have a Fraction
object, we will be able to access
its public operations.
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 section and one private section in a
class; however, C++ allows the keywords public:
and private:
to appear an arbitrary number of times in a class
declaration.
We have seen that methods 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
cout << greeting.size() << ' ' << closing.size() << endl;prints
5 7
, the sizes of the two string
s.
Object-oriented programmers like to think of a call to a method as a
message to which the object
responds. When we send the size()
message to greeting
, greeting
responds with the number of characters it
contains; and if we send the same size()
message to closing
, closing
responds with the number of characters it
contains. This is part of the internal perspective for designing and
writing a class. When we use the string
class, we're not
responsible for its operations. We just simple ask the string
objects to carry out various actions.
As you might expect, defining a method is also a bit different from defining a normal function.
pierre1.cpp
In this first part of this exercise, we will focus on adding the
methods to class Fraction
needed to get pierre1.cpp
operational.
To facilitate debugging a class, it is often a good idea to begin with a method that can be used to display class objects---an output method.
From the perspective of our Coordinate
class, we can specify
the task of such a method, which we'll call print()
, as
follows:
Specification:Note the use of internal perspective (e.g., "...I write my value.").receive:out
, anostream
to which I write my values.
output:myX
andmyY
, as an ordered pair.
passback:out
, containing the output values.
Also note that this method receives and passes back an ostream
. The rule is very simple:
Rule: All streams are passed by reference---received and passed back.
The reasons for this are fairly technical and are beyond what we really need to know right now, but this is hard-and-fast rule.
Methods must be prototyped within the class declaration, so we would write:
class Coordinate { public: void print(ostream & out) const; private: double myX, myY; };
Whoa! Where did that const
come from? Internal perspective
is an easy perspective for explaining this: "I am a Coordinate
object, and when I am printed using print()
, I
should not change." It's the const
that adds the "I should
not change". Think about it: a Coordinate
object that
changes every time we print it would be quite useless. We can get
the compiler to enforce this observation by adding const
after the parameter list for the method; now, the compiler will not
let us change myX
or myY
in the method definition.
Also, we've put print()
in the public section of the class.
This is quite typical. It's very common for programmers to print out
coordinates in their programs, so it's an operation that we want to
make available to everyone. Hence, it goes in the public section.
Whenever you declare a method, take a bit of time to think if the
method should change the instance variables of the class. If you
even suspect they shouldn't change, add the const
. You
can always take it away later if you discover you do need to
change the contents of an instance variable. (We'll see a method
that does very soon.)
This is a fairly simple method, so we would define it within the
header file Coordinate.h
, following the declaration of class
Coordinate
. As an inline method, we would precede its
definition with the keyword inline
.
To define print()
, we must inform the compiler that this is a
method of a class. This is done by preceding the name of the method
with the name of the class of which the method is a member and the
scope operator
(::). That is, we would define print()
as a method of our
Coordinate
class as follows:
inline void Coordinate::print(ostream & out) const { out << '(' << myX << ',' << myY << ')'; }
inline
indicates that this is a simple method that should
be inlined.void
tells the compiler that this method returns nothing.Coordinate::
tells the compiler that this method is a
member of class Coordinate
.print
is the name of the method.(ostream & out)
is the parameter list of the method.const
tells the compiler that this method should not
modify any of the instance variables of class Coordinate
.Note that const
must be present in both the prototype
and the definition of a method that does not modify its
instance variables. In contrast, inline
is used only
with the definition, not the prototype.
Let's consider what happens when this method is invoked. As with
anything class related, this makes the most sense in an internal
perspective: "I am a Coordinate
object, and when someone
sends me a message telling me to print myself out, I send a left
parenthesis, my x-coordinate, a comma, my y-coordinate, and a right
parenthesis to the provided ostream
." Note how the "my
" prefix makes the code similar to this statement; also note the
message metaphor.
Let's see if you have the hang of this:
Question #11.1: Ifpoint
is aCoordinate
object whose x-coordinate is 3 and whose y-coordinate is 4, then what does the statementpoint.print(cout);
display?
Question #11.2: Iforigin
is aCoordinate
object whose x-coordinate is 0 and whose y-coordinate is 0, then what does the statementorigin.print(cerr);
display?
As seen in the method definition above, a method can directly access
the private instance variables of the object. Otherwise, the private
instance variables would remain hidden to all code everywhere, making
them quite useless. Someone should be able to access them, and why
not myself? If I'm a Coordinate
object and someone asks me
to print myself, I should be allowed to access my private
instance variables.
Using all of this, prototype and define a similar print()
method for your Fraction
class. Write the method so that
when 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 method in the public section of class Fraction
, and define it as an inline
method following the
declaration of class Fraction
in Fraction.h
. Check
the syntax of your method, and continue when it is correct.
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 special function of a class called a constructor. A constructor specifies what actions are to be taken when a class object is constructed. When an instance is declared, the compiler calls this constructor function to initialize the object's instance variables.
For example, what should happen when we declare a new Coordinate
object like so:
Coordinate point;Presently, there would be junk in this object's instance variables. It seems reasonable, though, to initialize the x- and y-coordinates to 0.
We might specify this as a postcondition (as part of a specification):
Specification:postcondition:myX
== 0.0 andmyY
== 0.0.
A constructor does not return anything to its caller; it initializes the instance variables of an object when that object is defined. We specify this behavior as a boolean expression which is true when the constructor terminates. Such an expression is called a postcondition, since it is a condition that holds true after (i.e., "post") the constructor finishes.
A postcondition is not code you should write. It's a test that
should be true if you tested it at the end of the constructor.
You can test a postcondition if you want (using an assert()
), and this may be a good idea (especially if there's a lot
of code and there's a bug you can't find). But it's not necessary.
Instead, a postcondition indicates what other code you should
write. In this case, what code can I execute so that myX
is 0.0? The answer is below.
In order for a constructor to be a member of a class, its prototype must appear within the class. Unlike other functions, C++ determines the name of this function:
Rule: The name of a constructor is always the name of the class.
So we prototype this constructor in the public section of class
Coordinate
, as follows:
class Coordinate { public: Coordinate(); void print(ostream & out) const; private: double myX, myY; };
Unlike our print()
method, a constructor initializes (i.e.,
modifies) the instance variables of a class. As a result, it should
not (and cannot) be prototyped or defined as const
. So
the "missing" const
is understandable.
However, where's the return type?!! Every function has had a return type. C++ insists on it. Well, in fact, C++ will equally insist that a constructor does not have a return type. As observed above, a constructor never returns anything to its caller; it initializes its instance variables. The object itself has already been created, so that doesn't have to be returned. It just needs to be initialized. So constructors do not need a return type.
As with the print()
method, we want everyone to be able to
construct Coordinate
objects, so the prototype should be
placed in the public section of the class.
Also, as before, simple definitions should be placed in the class
header file, designated as inline functions. To define an inline
Coordinate
constructor, we thus write this funny-looking
definition in the header file:
inline Coordinate::Coordinate() { myX = 0.0; myY = 0.0; }The first
Coordinate
is the name of the class, telling the
compiler that this is some member of class Coordinate
. The
second Coordinate
is the name of the constructor.
Given this definition, when a Coordinate
object is defined,
the compiler will call this constructor to initialize the new object,
setting the object's myX
and myY
instance variables
to zero.
The pattern for a constructor is thus:
where the firstClassName
::ClassName
(ParameterList
) {StatementList
}
ClassName
refers to the name of the class,
the second ClassName
names the constructor, and StatementList
is a sequence of statements that initialize the
instance variables 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 constructor.
Using this information, prototype and define a constructor for your
Fraction
class, that satisfies the following specification:
Specification:That is, the definition:postcondition:myNumerator
== 0 andmyDenominator
== 1.
Fraction oldMeasure;should initialize the instance variables of
oldMeasure
appropriately to represent the fraction 0/1. (Remember that division
by 0 is a Bad Thing, so initializing it to 0/0 is not advisable.)
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.
A class can have multiple constructors, so long as each definition is distinct in either the number or the type of its parameters. Defining the same function multiple times is called overloading the function. Overloading works for normal functions, methods, and constructors.
Suppose that we would like to be able to explicitly initialize the x-
and y-coordinates of a Coordinate
object to two values
specified by the programmer creating the object.
We can specify this as follows:
Specification:receive:x
andy
, twodouble
values.
postcondition:myX
==x
andmyY
==y
.
We can overload the Coordinate
constructor with a second
definition, one that takes two double
arguments and uses them
to initialize our instance variables:
inline Coordinate::Coordinate(double x, double y) { myX = x; myY = y; }
Note the benefit of using the "my
" prefix: we don't have to
come up with silly or awkward names for the parameters here. If we
called the instance variables x
and y
, we'd have to
come up with different names for these parameters. This convention
also clarifies the internal perspective: myX
is my
x-coordinate while x
is an x-coordinate that something else
has handed to me.
As usual for such a simple function, this constructor is defined
inline in the header file, and like all methods of a class, its
prototype would be placed in the public section of class Coordinate
:
class Coordinate { public: Coordinate(); Coordinate(double x, double y); void print(ostream & out) const; private: double myX, myY; };We can declare
Coordinate
objects to
invoke this constructor:
Coordinate point1, point2(1.2, 3.4);Our first constructor initializes
point1
since its
declaration has no arguments and the first constructor has no
parameters. Our second constructor initializes point2
since
its declaration has two double
arguments and the second
constructor has two double
parameters. After these
declarations, we have these objects:
Using this information, define and prototype a second Fraction
constructor that satisfies this specification:
Specification:Consequently, the definitionsreceive:numerator
anddenominator
, two integers.
precondition:denominator
is not 0.
postcondition:myNumerator
==numerator
andmyDenominator
==denominator
.
Fraction oldMeasure; ... Fraction scaleFactor(1, 6);should initialize
oldMeasure
to 0/1, and initialize scaleFactor
to 1/6.
Use a call to assert()
to ensure the precondition.
Use the compiler to test the syntax of what you have written. When
the syntax is correct, use pierre1.cpp
to test what you have
done, by inserting calls to print()
to display their values:
... oldMeasure.print(cout); ... scaleFactor.print(cout);
Also try initializing scaleFactor
with a zero denominator.
Make sure that the assert()
is triggered.
When your constructors and methods are working correctly, remove this
test code from pierre1.cpp
.
It might be useful to be able to extract the x- and y-coordinates of
a Coordinate
object. It is common to need the values stored
in an object, and the methods that return these values are generally
known as accessor
methods.
As with most classes, the accessor methods for the Coordinate
class have very simple specifications:
Specification:andreturn: my x-coordinate.
Specification:return: my y-coordinate.
Since these methods do not modify any of the instance variables, we
can declare them as const
methods. Generally, accessor
methods begin with the prefix "get
" followed by the name of
the attribute (without "my
"). So, two new prototypes
are added:
class Coordinate { public: Coordinate(); Coordinate(double x, double y); double getX() const; double getY() const; void print(ostream & out) const; private: double myX, myY; };
We would then define these simple methods in the header file as follows:
inline double Coordinate::getX() const { return myX; } inline double Coordinate::getY() const { return myY; }
Suppose we declare two Coordinate
objects point1
and
point2
:
Question #11.3: What do these expression evaluate to:point1.getX()
andpoint2.getY()
?
This is another reason for the "my
" prefix. We can use the
my
-free names as the name of a method that accesses the value
of that instance variable.
Using this information, add to class Fraction
an accessor
method getNumerator()
that satisfies this specification:
Specification:Also write an accessor methodreturn:myNumerator
.
getDenominator()
that satisfies
this specification:
Specification:Since these are simple methods, define themreturn:myDenominator
.
inline
following
the class declaration in the header file. Test their syntax by
compiling the code and continue when they are correct.
Once we are able to define Fraction
objects, it is useful to
be able to input a Fraction
value. To illustrate, suppose
that we wanted to input a Coordinate
value that looked like
this:
(3,4)A user would type this in, or perhaps a program would read this in from a file.
We can specify the problem as follows:
Specification:receive:in
, anistream.
precondition:in
contains aCoordinate
of the form(x,y)
.
input:(x,y)
, fromin
.
passback:in
, with the input values extracted from it.
postcondition:myX
==x
&&myY
==y
.
We prototype this method in the class (as with all methods), although
this one is not const
because it modifies the instance
variables:
class Coordinate { public: Coordinate(); Coordinate(double x, double y); double getX() const; double getY() const; void read(istream & in); void print(ostream & out) const; private: double myX, myY; };
We can define read()
as a method that satisfies the
specification, as follows:
void Coordinate::read(istream & in) { char ch; // for reading ( , and ) in >> ch // read '(' >> myX // read x-coordinate >> ch // read ',' >> myY // read y-coordinate >> ch; // read ')' }
Testing pre- and postconditions for an input method is tricky at best, so don't worry that we haven't tested them here. They're good to write down anyway so that we're clear on what we expect.
The input specification indicates that a coordinate in the input will
have punctuation around it: parentheses and a comma. The ch
variable is used to read in these characters and throw them away.
It's useful to have them in the input to make the input more
readable, but they're frivolous in a Coordinate
object since
every Coordinate
object is written with this
punctuation. We only need it in the input and output.
Given the length of this method, it's pressing the boundaries of what
some compilers define as "simple"; some won't allow us to declare it
inline. As a result, we define it in the implementation file
(without the keyword inline
).
With this method, the statements
Coordinate point; point.read(cin);reads a
Coordinate
of the form (x,y)
from cin
.
Using this information, define and prototype an input method named
read()
for class Fraction
. Your method should
satisfy this specification:
Specification:receive:in
, anistream
.
precondition:in
contains a Fraction value of the formn/d
andd
is not zero.
input:n/d
, fromin
.
passback:in
, withFraction
n/d
extracted from it.
postcondition:myNumerator
==n
andmyDenominator
==d
.
Some differences from the read()
of Coordinate
:
d
is not zero). Use a call to assert()
.After you've written and successfully compiled this new method, you should be able to uncomment the statements:
oldMeasure.read(cin); ... scaleFactor.read(cin);
Test your input method by adding print()
statements after the
read()
statements in pierre1.cpp
to test the input
routines. Compile and run the program, and continue when read()
works correctly. Remove the test print()
statements.
We have seen that methods like constructors can be overloaded. In
addition, C++ allows us to overload operators, such as the arithmetic
operators (+
, -
, *
, /
, and %
). However to do so, we need to rethink the way expressions work.
Suppose that we want to permit two Coordinate
objects to be
added together. In C++'s object-oriented world, an expression like
point1 + point2is thought of as sending the
+
message to point1
,
with point2
as a message argument. We can specify the
problem from the perspective of the Coordinate
receiving this
message:
Specification:receive:point2
, a Coordinate.
return:result
, a Coordinate.
postcondition:result.myX
==myX
+point2.myX
andresult.myY
==myY
+point2.myY
.
Again, the postcondition here suggests the code that we should write, and it doesn't particularly warrant testing at the end of the method.
According to our specification, this operation does not modify the
instance variables of the Coordinate
that receives it, and so
we write the following prototype:
class Coordinate { public: Coordinate(); Coordinate(double x, double y); double getX() const; double getY() const; void read(istream & in); void print(ostream & out) const; Coordinate operator+(const Coordinate & point2) const; private: double myX, myY; };
Not only is the method itself const
, but the parameter is
const
as well. We don't change either object. We pass the
parameter by reference because it's not a primitive type; recall that
passing by reference is a bit faster than by value for non-primitive
types.
One way to define this method is as follows:
Coordinate Coordinate::operator+(const Coordinate & point2) const { Coordinate result(myX + point2.getX(), myY + point2.getY()); return result; }This definition uses our second constructor to construct and initialize
result
with the appropriate values.
This function illustrates that, for any overloadable operator
Δ, we can use the notation operatorΔ
as the name
of a function that overloads Δ with a new definition.
Once such a method has been prototyped and defined as a member of
class Coordinate
, we can write familiar looking expressions,
such as
point1 + point2to compute the sum of two
Coordinate
objects point1
and point2
.
The C++ compiler treats such an expression as an alternative notation for the method call:
point1.operator+(point2)
While it is useful to overload all of the arithmetic operators for a
Fraction
, the particular operation that we need in order to
solve our problem is multiplication (the others, we leave for the
exercises). From the preceding discussion, it should be evident that
we need to overload operator*
so that the expression in
pierre1.cpp
:
oldMeasure * scaleFactorcan be used to multiply the two
Fraction
objects oldMeasure
and scaleFactor
.
However, the math here is a bit more involved that in the past examples and tasks. We can get some insight into the problem by working some simple examples:
The specification for such an operation can be written as follows:
Specification:receive:rightOperand
, aFraction
operand.
return:result
, aFraction
, containing the product of the receiver of this message andrightOperand
, simplified, if necessary.
We can construct result
by taking the product of the
corresponding instance variables and then simplifying the resulting
Fraction
.
For the moment, ignore the problem of simplifying a Fraction
.
Extend your Fraction
class with a definition of operator*
that can be used to multiply two Fraction
objects.
When we add the code to simplify result
, this method will
be reasonably complicated, so define it in the implementation file
(and, of course, prototype it in the header file, in the class
definition). Test the correctness of what you have written by
uncommenting the lines in pierre1.cpp
that that compute and
output newMeasure
. Continue when your multiplication
operation yields correct (if unsimplified) results.
The main deficiency of our implementation of operator*
is its
inability to simplify improper fractions. That is, our
multiplication operation would be improved if class Fraction
had a simplify()
operation, such that fractions like:
2/6, 6/12, 12/4 could be simplified to 1/3, 1/2, and 3/1, respectively.
Such an operation is useful to keep fractional results as simple and
easy to read as possible. To provide this capability, we will
implement a Fraction
method named simplify()
. In a
method like operator*
which constructs its answer in a Fraction
named result
, simplify()
can be invoked
like so:
result.simplify();to simplify the
Fraction
in result
.
It will help to phrase this in message passing terms. This calls
says, "Hey, result
! Simplify yourself!" result
then
goes off and simplifies itself (e.g., changes itself from 12/4 to
3/1). I don't expect anything back; I've told result
to do
all the work and change itself.
That takes care of result
in the multiplication operation,
but what about the definition of simplify()
? We shift our
perspective to the Fraction
that's been told to simplify
itself. There are a number of ways to simplify a fraction. One way
is the following algorithm:
gcd
, the greatest common divisor of myNumerator
and myDenominator
.myNumerator
by myNumerator/gcd
.myDenominator
by myDenominator/gcd
.Those "replace" steps are assignment statements: e.g., "Change my numerator to be my numerator (my old one) divided by the GCD." Read this statement carefully, and the code writes itself.
The specification for this method is thus:
Specification:postcondition:myNumerator
andmyDenominator
do not share an common factors (i.e., the fraction is simplified).
Remember that simplify()
doesn't need any extra information,
except the Fraction
object that it's invoked on, so there are
no parameters. Also, the object that receives this message changes
itself, so it cannot be declared const
. And there's no
need to return anything. So we end up with only a postcondition in
our specification.
The implementation file gcd.cpp
contains a function greatestCommonDivisor()
that implements Euclid's algorithm for
finding the greatest common divisor of two integers. Using greatestCommonDivisor()
and the preceding algorithm, define simplify()
as a method of class Fraction
.
Since this is a complicated operation, define it in the implementation file, and only prototype it in the header file.
We have now provided all of the operations needed by pierre1.cpp
, so the complete program should be operable and can be
used to test the operations of our Fraction
class.
pierre2.cpp
In this second part of this exercise, we add the functionality to
class Fraction
in order for pierre2.cpp
to work
properly, so use your text editor to open it for editing.
While we have provided the capability to output a Fraction
value via a print()
method, doing so requires that we write
clumsy code like:
cout << "\nThe converted measurement is: "; newMeasure.print(cout); cout << "\n\n";instead of elegant code like:
cout << "\nThe converted measurement is: " << newMeasure << "\n\n";
Our print()
method does solve the problem, but its solution
doesn't fit in particularly well with the rest of the iostream
library operations. It would be preferable if we could use
the usual insertion operator (<<
) to display a Fraction
value.
Let's revisit our Coordinate
class. We would like to be able
to write this:
cout << point << endl;And this should display the
Coordinate
object named point
on cout
.
Well, <<
is an operator just like +
or *
, and
overloading those worked well, so we could add a method to class
ostream
overloading operator<<
with a new definition
to display a Coordinate
value. Then the compiler could treat
an expression like
cout << pointas the call
cout.operator<<(point)However, this would require us to modify
ostream
, a
predefined, standardized class. This is never a good idea,
since the resulting class will no longer be standardized. (It's also
very hard to do.)
We could define operator<<
as a method of Coordinate
, but C++ would then require us to invoke the method like
so:
point << cout;Yikes! That looks like
cout
is being sent to point
---the complete opposite of what we want. Technically,
we could use the >>
operator instead, but we'd still have the
output stream on the right, and we're used to our streams on the far
left. Everyone keeps their streams on the left.
Instead, we can overload the insertion operator (<<
) as a
normal function (i.e., not as a method) that takes an ostream
(e.g., cout
) and a Coordinate
(e.g., point
) as its operands. That is, an expression
cout << pointwill be translated by the compiler as a normal function call
operator<<(cout, point)That's exactly what we want.
We define the following function in the header file, following the class declaration:
inline ostream & operator<<(ostream & out, const Coordinate & coord) { coord.print(out); return out; }
There are several subtle points in this definition that need further explanation:
inline
function. Since it is not a method, defining it
in the header file serves as both prototype and definition for the
function. An inline method must still be prototyped in the class
so that the compiler fully understands that it is a member of the
class.ostream
which is altered by the operation---the Coordinate
is inserted into the output stream. Since it's changed, the ostream
is a non-const
reference parameter.const
.cout <<The leftmostValue1
<<Value2
<< ... <<ValueN
;
<<
is applied first, and the value it returns
becomes the left operand to the second <<
. Similarly, the
value returned by the second <<
becomes the left operand of
the third <<
, and so on, down the chain. Consequently, our
function must return an ostream
to its caller to make the
chaining work correctly. However, if we simply make the
return-type ostream
(instead of ostream&
), the C++
function-return mechanism will make and return a copy of
parameter out
which (as an alias for cout
) would
return a copy of cout
for use by the next operator in the
chain. As a result, the next value would get inserted into a copy
of cout
, rather than cout
itself, which would have
unpredictable results.
What we need is a way to tell the compiler to return the
actual object to which out
refers, instead of a copy of
it. This is accomplished by defining the function with a reference
return-type, ostream&
. The compiler then does all the
work to return the actual object itself (not a copy), and so the
chaining will work as we want it to.
Coordinate
is done by sending the Coordinate
parameter coord
the print()
message we defined in Part I of this exercise,
with out
as its argument. If we hadn't written print()
, we could still define this function using the accessor
methods getNumerator()
and getDenominator()
.For our Fraction
class, the specification of this operation is thus:
Specification:receive:out
, anostream
, andaFraction
, aFraction
.
precondition:==
aFraction
.getNumerator()n
and==
aFraction
.getDenominator()d
.
output:aFraction
, in the formn/d
, viaout
.
passback:out
, containingn/d
.
return:out
, for chaining.
Using this information, overload the output operator for class Fraction
. Uncomment the appropriate line in pierre2.cpp
,
and test it out. You haven't written the input operator yet, so don't uncomment those lines yet. Just use the default values in the
Fraction
s. Continue when pierre2
compiles and
executes correctly (as far as it should for now).
As a dual to output, all of the things we learned about the output operator also apply to the input operator, just with a slight change in direction (reading information in, instead of sending it out). The syntax is very similar.
Suppose we wanted to input a Coordinate
, entered as
(3,4)We could define this operator in the header file:
inline istream & operator>>(istream & in, Coordinate & coord) { coord.read(in); return in; }There are a few differences between the input and output (i.e., extractor and insertion) operators:
operator<<
, the extraction
operator is operator>>
.istream
instead of an ostream
.Coordinate
reference, rather than a
const
Coordinate
reference. Since we want to change this Coordinate
, it cannot be const
.istream
reference
instead of an ostream
reference. Similar to the output
operator, this is for chaining: cin >> point1 >> point2;
.Using this information, overload the extraction operator for class
Fraction
, so that a user can enter
3/4to input the fraction 3/4.
Uncomment the remaining statements in pierre2.cpp
, and test
the correctness of these operations. Your Fraction
class
should now have sufficient functionality for Chef Pièrre to solve his
problem using pierre2
.
When everything in your Fraction
class is correct, complete
Fraction.doc
.
While it is not necessary for this particular problem and the code solution we have, there are certain situations where it is useful for a function that is not a member of a class to be able to access the private instance variables. By default, C++ will not allow any other function to access private instance variables.
But suppose we had not written the read()
method for class
Coordinate
, and wanted to overload operator>>
in
order to input Coordinate
values. We'd probably write what
we do have for read()
in the input operator:
istream & operator>>(istream & in, Coordinate & coord) { char ch; in >> ch // consume ( >> coord.myX // read x-coordinate >> ch // consume , >> coord.myY // read y-coordinate >> ch; // consume ) }Syntactically, this is 100% correct. However, semantically, the compiler does not allow this and will generate compilation errors for the accesses to
coord
's instance variables. As a non-method,
this operator does not have the privilege of accessing the private
instance variables. However, we might like to be able to grant this
permission.
For such situations, C++ provides the friend mechanism. If a class names a
function as a friend with the keyword friend
, then that
non-member function is permitted to access the private section of the
class. A function is declared as a friend by including a prototype
of the function preceded by the keyword friend
in the class
definition. Thus, we would have to write this in our Coordinate
class:
class Coordinate { public: Coordinate(); Coordinate(double x, double y); double getX() const; double getY() const; Coordinate operator+(const Coordinate & point2) const; friend istream & operator>>(istream & in, Coordinate & coord); private: double myX, myY; };
Placing the friend
keyword before a function prototype in a
class thus has two effects:
As we have seen in this exercise, an object-oriented programmer can
usually find other ways to implement an operation without resorting
to the friend
mechanism. In the object-oriented world,
solving a problem through the use of methods is generally preferred
to solving it through use of the friend
mechanism. As a
result, friend
functions tend to be used only when the
object-oriented alternatives are too inefficient for a given
situation. Nevertheless, they are a part of the C++ language, and
you should know the distinction between a method and a friend
function of a class.
You could make a similar change to your Fraction
class, even
just prototype the input and output operators as friend
functions, but it's not necessary.
Now that you have seen how to build a class, we need to expand our design methodology to incorporate classes:
Using this methodology and the C++ class mechanism, we can now create a software model of any object! If you can imagine it, you can write a class for it.
The class thus provides the foundation for object-oriented programming, and mastering the use of classes is essential for anyone wishing to program in the object-oriented world.
Learning to design and implement classes is an acquired skill, so feel free to practice by creating software models of objects you see in the world around you! You cannot practice this too much.
Turn in a copy of your code and sample executions of both
drivers. Answer the questions posed in this lab exercise in comments
in the Fraction.doc
documentation file.