In most of the problems we have worked on in these lab exercises,
the data objects could be represented using types provided in C++. For
example, we might represent the radius of a circle with a
double
; menu selections with a char
or an
int
; the radius of a circle with a double
; and
an element of a Life grid with a short int
.
However, there are many real-world problems with data objects
that cannot be directly represented using just a single predefined
C++ type. Here are two such problems:
Problem #1: Cartesian points. In mathematics courses, functions of a single variable can be pictured with two-dimensional graphs. Two-dimensional vectors used in a calculus or physics course and operations on them such as addition, subtraction, and scalar multiplication can also be pictured by two-dimensional plots. These graphs typically are plotted in a Cartesian coordinate system using x- and y-coordinates for points on the graph. The following figure illustrates plotting a point with coordinates (7,3), which is 7 units to the right along the x-axis and 3 units up along the y-axis:
Problem #2: Fractions. The second problem, and the one that we will be concerned with in this lab exercise, is to develop a fraction calculator, that is, a calculator in which the user enters fractions such as 1/4 and 2/3 and specifies some operation to be performed on them such as multiplication, for which the calculator will output the result as a (reduced) fraction: 1/6.
Building the fraction calculator would be quite easy if we had a
data type named Fraction
that we could use to declare fractions,
input them, output them, and perform arithmetic operations such as
multiplication on them. And that is what you will be doing in this lab
exercise — building a Fraction
class. The example used to
help you is building a Coordinate
type for points in the plane.
Fraction.h
,
Fraction.cpp
, and
Fraction.txt
implements the Fraction
class.gcd.h
and gcd.cpp
implement
Euclid's greatest-common-divisor algorithm (used by the Fraction
class).fractionTester.cpp
is a program to test-drive the Fraction
class.C++ does not provide a Temperature
type or a
Fraction
type or . . . types for a lot of other "real-world"
things. However, it does provide a class mechanism that we
can use to extend the language by creating a new type, including built-in
operations for that type. This new type will provide:
For example, for a Temperature
class we might have
double
to store the degrees
char
to store the scale ('C' for Celsius, 'F' for
Fahrenheit, etc.)
Temperature
object
Temperature
object
These two kinds of members of a class are usually organized into two sections in a class declaration of the form
Such a declaration is (almost always) saved in a header file namedclassTypeName
{ public: // -- Declarations of function members private: // -- Declarations of data members };
Typename.h
. It creates a new data type
named TypeName
.
Note that a class declaration has two sections: a
public section where function members that implement
the class operations are declared; and a private section
where the data members that implement the class attributes are declared.
The contents of the public section are accessible to a program that
#include
s the file containing the class declaration and the
contents of the private section are not (unless some
public function member makes them accessible).
As far as the compiler is concerned, it doesn't matter whether the public section is first and the private section last or the order is reversed. Here, as in the textbook, we will put the public section first because it is the part of the class that is accessible in programs that use the class — the public interface to the class. The private section, whose contents are not accessible (except via public function members), is "buried" at the bottom.
Let's look now at our example of Cartesian coordinates of points in two dimensions. They consist of an x-coordinate and a y-coordinate. We can easily declare two variables to store these values,
double myX, myY;
and put these in the private section of an appropriately-named class, making them data members:
class Coordinate { public: private: double myX, myY; };
Using the prefix "my
" at the beginning of a name
for a data member name helps maintain an internal
perspective. We imagine ourselves as the object we're trying to
describe. For example, when deciding what the data members should be, I
could say, "I am a Cartesian coordinate, and I have an
x-coordinate and a y-coordinate." And because they
are my attributes, I label them as such. Although this may sound
rather inane and superfluous, it does at least help keep us from confusing
them with other objects that we'll be using.
The result is a new type, named Coordinate
,
that can be used to declare new objects. For example,
Coordinate point1, point2, point3;
creates three Coordinate
objects that we might picture
as
Each of the Coordinate
objects point1
,
point2
, point3
,
has two double
components,
one named myX
and the other named myY
.
Every Coordinate
object 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:
where each class
TypeName
{
public:
private:
Type1
DataMemberList1
;
Type2
DataMemberList2
;
...
Typen
DataMemberListn
;
};Typei
is any defined type and each
DataMemberListi
is a list of data members of
type Typei
.
Now let's apply a similar approach to the fraction problem. We first
need to identify the attributes of a Fraction
object. Here
are a few fractions:
1/2Each fraction has the form:
4/3
4/16 16/4
number1 / number2where number1 is the numerator and number2, which must be nonzero, is the denominator. A fraction needs both a numerator and a denominator. But the slash (/) symbol serves only to separate them and could as well be some other character such as ] or \ or a host of other symbols; it isn't really an attribute of a fraction. Consequently, a fraction has just two attributes, a numerator and a denominator, both of which are integers and the denominator is nonzero.
Question #12.1: Edit the fileFraction.h
and do the following:Look at the patterns and example above if you need help.
- Write a class declaration for a class named
Fraction
.- In the appropriate place, define two
int
data members namedmyNumerator
andmyDenominator
.
Given this definition of a new type Fraction
,
we can make declarations like the following:
Fraction frac1, frac2, frac3;These declarations define three
Fraction
objects with the
following forms:
Again, note that each object of type Fraction
has its own
copy of each of the data members myNumerator
and myDenominator
; that's why we prefixed the variable names
with "my
" — to encourage an internal perspective.
Question #12.2: In the source programfractionTester.cpp
, uncomment the declaration of theFraction
objectf
:
Fraction f;(Do not uncomment anything else yet.) Compile, link, and execute your source program to test the syntax of what you have written. When it is correct, continue to the next part of the exercise.
In addition to data members, a class can also have function members. A function member is a function declared inside a class to provide an operation for objects of the class.
We've used several function members already — for example, a
string
object has
find()
, length()
, and substr()
function members plus many others;
a vector
object has many function members such as
front()
, push_back()
, and size()
.
But until now we've only used function members. Now we have to
define our own function members in our own classes.
Function members are prototyped within the class structure itself (in the
header file), and they are normally defined in the class
implementation file. However, very simple function members (i.e., those
that have at most 3 or 4 operations) are usually defined
"inline" in the header file, following the
class declaration, prefixed by the keyword
inline
. (The reason for this is that the
code for inlined functions is inserted wherever they are called,
thus avoiding the extra overhead involved in function calls.)
One of the characteristics of a class is that its data members
are kept private, meaning that a
program using the class is not allowed direct access to them.
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
data members.
However, in the Fraction
class, we must make sure that
myDenominator
does not become zero.
If we hide myDenominator
from the casual
programmer, we can write the function members of the Fraction
class so that myDenominator
is never 0.
In practice, it is not worth the time and effort to try to figure out
which data members are safe to let everyone access directly.
Common practice is to make all data members private.
On the other hand, with few exceptions, we do want users of the
class to be able to access the operations of a class, because they provide
the interface to objects of that type. Thus, the
operations are declared in the public section
of the class. So wherever we have a Fraction
object,
we will be able to access its public operations. (If there are some
"special" functions that perform operations such as encryption, we
could prevent access to them by putting them in a separate private section.)
As noted earlier, by convention, the public section of a class comes
first in a class declaration, so that a user of the class can quickly see
what operations it provides — this is why it is referred to as the
interface to the class. It is good style to have one public
section and one private section in a class; however, C++ does allow the
keywords public:
and private:
to appear an
arbitrary number of times in a class declaration.
We have seen that function members are called differently than normal
functions — by using the dot operator
(the "push-button" operator). For example, if two string
objects named greeting
and closing
are defined
as follows
string greeting = "hello", closing = "goodbye";then the output statement
cout << greeting.size() << ' ' << closing.size() << endl;will display
5 7
, the sizes of the two string
s.
From an object-oriented perspective, we can think of calling a function
member as sending 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 simply ask the string
objects to carry
out various actions.
A useful analogy is that of a wristwatch that has a built-in calculator and push buttons to perform basic arithmetic operations. Calling a function member of an object is like pushing a button on this watch to send it a message to add two numbers that you enter. This is very different from the "ordinary" functions we considered in earlier lab exercises — with them, we would have to take our watch, wrap it up in a package and send it to a watch-processing facility who would know how to have the watch display the sum of our two numbers and communicate that back to us when then return our watch.
Fraction
Class — Part ICoordinate
class and then having you do this for your Fraction
class.
In this first part of this exercise, we will focus on adding some of the
usual function members to the Fraction
class — output, input,
constructors, accessors — and one arithmetic operation —
multiplication. (Project 12.1 asks you to add others.) You will use
the program fractionTester.cpp
to test your operations.
To help with 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 member — because we can use it to display the results produced by other operations.
From the perspective of our Coordinate
class, we can specify
the task of such a function member, which we'll call display()
, as
follows:
Specification:Note the use of internal perspective — e.g., "... I write my value." — used because we are defining a built-in operation.
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 function member receives and passes back an ostream
.
Rule: All streams are passed by reference — received and passed back.
The reason for this is that when a stream is received by a function as one of its parameters and the function changes this stream — e.g., removing something from it via input or adding to it via output — this same change must happen in the corresponding stream argument.
Function members must be prototyped within the class declaration, so we would write:
class Coordinate { public: void display(ostream & out) const; private: double myX, myY; };
No, the keyword const
at the end of the prototype for
display()
isn't a mistake. We've used it before with parameters
to keep them from being changed by a function and it plays a somewhat
similar role here.
Using an internal perspective is the easiest way to explain this:
"I am a Coordinate
object, and when I am output by
display()
, I cannot be changed." It's the const
that adds the "I cannot be changed." And this is surely what we
want because a Coordinate
object that changes every time it
is displayed would be quite useless. Adding the const
at the
end of the function heading instructs the compiler to protect
Coordinate
objects from being changed in the definition of
the display()
function. Any attempt to change myX
or myY
in the definition of display()
either
directly or by sending them to some other function that could change them
will be rejected by the compiler.
Also, note that we've put display()
in the public section
of the Coordinate
class. The reason for is that we want
it to be available to anyone who uses this class. Function members
that are only to be used internally in the class and not made available
to users can be placed in the private section.
This is a simple function member so we probably would want to inline it
to avoid the overhead of calls to it. Recall that inlining a function
makes its definition available to the compiler so that it can substitute
it for calls to that function. And to make this definition available to
the compiler, we put it in the header file Coordinate.h
,
following the definition of the class.
However, we must also inform the compiler that display()
is a
function member of the Coordinate
class so it can bind this
function definition to its prototype. And this is done by attaching the
name of the class to the name of function with the
scope operator :: . That is, we define
display()
as a function member of our
Coordinate
class as follows:
inline void Coordinate::display(ostream & out) const
{
out << '(' << myX << ',' << myY << ')';
}
inline
indicates that this is a simple function
that should be inlined. Note that it it is used only with the
the definiition, not the prorotype.void
informs the compiler that this function returns
nothing.Coordinate::
informs the compiler that this function member
is a member of class Coordinate
.display
is the name of the function member.(ostream & out)
is the parameter list of the function
member.const
tells the compiler that this function member should
not modify any of the data members of class Coordinate
.
Now try the following to check your understanding of this rather lengthy presentation.
Question #12.2: Ifpoint
is aCoordinate
object with x-coordinate 3 and y-coordinate 4, what output (if any) will be produced by the statementpoint.display(cout);
Question #12.3: Iforigin
is aCoordinate
object with x-coordinate 0 and y-coordinate 0, what output (if any) will be produced by the statementpoint.display(cerr);
Using the preceding as a guideline:
Fraction Coding #1:
- Add a prototype and defininition of a similar
display()
function member for yourFraction
class. Write the function member so that whenf
is aFraction
whose numerator is 3 and whose denominator is 4, then a message:f.display(cout);will display3/4
- Prototype this function member in the public section of class
Fraction
, and define it as aninline
function member following the declaration of classFraction
inFraction.h
.
- To check what you have done, uncomment the code in the first section of
fractionTester.cpp
below//1. Test display() function
. You can either physically remove the comment delimiters/*
and*/
or "virtually" remove them by making each line begin with//
. Then compile, link, and execute the program. Proceed to the next part of this lab exercise you are satisfied with the format of your output.
An output operation for a class is of little use unless we can build objects of that class. This is the task of a special function member called a constructor. It specifies what actions to take when a class object needs to be created. For example, when an object is declared as in
the compiler will call a constructor function to initialize the data members of that object —Coordinate point;
point
in this example. Currently,
there would be "garbage" in this object's data members, but it would
seem preferable to have them initialized to some default values. And in
this example, the origin (0, 0) seems like a reasonable choice.
We can specify this as a postcondition (as part of a
specification):
Specification:
Postcondition:myX
== 0 andmyY
== 0.
A constructor does not return anything to its caller; it initializes the data members of an object when that object is declared. We specify this behavior as a boolean expression that will be true when the constructor terminates. Such an expression is called a postcondition because it is true after (i.e., "post") the constructor is executed.
Like other function members, a constructor's prototype must be within the class declaration. But a constructor has some unique requirements.
And because it is a member of a class, its prototype must appear within the class. So, for our
Rule: The name of a constructor is always the name of the class. It has no return type — not even void
.
Coordinate
class, the constructor's
name is Coordinate()
and we prototype it in the public
section of class Coordinate
:
class Coordinate { public: Coordinate(); void display(ostream & out) const; private: double myX, myY; };
Note that the const
specifier is not used with the
constructor because, unlike our display()
function member,
a constructor initializes and thus modifies the class' data members.
As a result, it cannot be specified as const
.
Also, as with the display()
function member, the
prototype should be placed in the public section of the class so that
users of this class can construct Coordinate
objects.
To define an inline Coordinate
constructor, we proceed as
with other function members and qualify their names with the class name,
which gives a rather unusual-looking definition in the header file,
placed after the end of the class declaration:
The firstinline Coordinate::Coordinate() { myX = 0.0; myY = 0.0; }
Coordinate
is the name of the class, informing the
compiler that this is a member of class Coordinate
. The
second Coordinate
is the name of the constructor.
Given this definition, when a Coordinate
object is declared,
the compiler will call this constructor to initialize the new object,
setting it's myX
and myY
data members to zero.
The general pattern for the definition of a constructor is:
where the first ClassName
::ClassName
(ParameterList
)
{
StatementList
}ClassName
refers to the name of
the class, the second ClassName
names the
constructor, ParameterList
is a sequence
(possibly empty) of parameter declarations, and
StatementList
is a sequence of statements
that initialize the data members of the class. As this pattern indicates,
constructors may have no parameters — for example, the above
constructor for the Coordinate
class. Such a constructor
is called the class' default-value constructor. A
constructor that does have parameters is an
explicit-value constructor. We will illustrate this
in the next section.
Using this information:
Fraction Coding #2:
- Prototype and define a default-value constructor for your
Fraction
class, using the following specification:Specification:That is, the declaration
Postcondition:myNumerator
== 0 andmyDenominator
== 1.Fraction f;should initialize the data members off
appropriately to represent the fraction 0/1.- Put the prototype in the public section of class
Fraction
, and its definition, specified asinline
, after the end of the class declaration inFraction.h
.- To test what you have written, uncomment the code in
fractionTester.cpp
in the section labeled2. Test Constructor #1
and then compile, link, and execute the program. Continue when it executes correctly.
In C++, a function may be defined more than once, provided that each definition differs from the others in the number or the types of its parameters. Defining the same function multiple times in this way is called overloading that function. Overloading works for ordinary functions as well as for function members, including constructors, of a class.
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, an explicit-value constructor that has two
double
parameters and uses them to initialize our data members:
inline Coordinate::Coordinate(double x, double y) { myX = x; myY = y; }
As usual for such simple functions, this constructor is defined inline after the class declaration in the header file and like all function members of a class, its prototype placed in the public section:
We can now declareclass Coordinate { public: Coordinate(); Coordinate(double x, double y); void display(ostream & out) const; private: double myX, myY; };
Coordinate
objects and initialize them by
using this initial-value constructor; for example,
The default-value constructor initializesCoordinate point1, point2(1.2, 3.4);
point1
because its
declaration has no arguments. Our initial-value constructor initializes
point2
because its declaration has two double
arguments and the second constructor has two double
parameters.
After these declarations, we have these objects:
Fraction Coding #3:
- Define and prototype a second
Fraction
constructor that satisfies this specification:
Specification:
Return:myNumerator
.
- Also write an accessor function member
getDenominator()
that satisfies this specification:Specification:These are simple function members so define them
Return:myDenominator
.inline
after the class declaration in the header file.- To test these extractors, uncomment the code in the section of
fractionTester.cpp
labeled4. Test Extractor Functions
, and then compile, link, and execute the program. When everything is working correctly, proceed to the next part of this lab exercise.
Now that we are able to define Coordinate
objects and
Fraction
objects, it would be nice if we could input
them. For example, suppose we wanted to input a Coordinate
value as
(3,4)A user would enter this from the keyboard or perhaps a program would read it 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
andmyY
==y
.
As with all function members, we prototype this function member in the
class, but this one is not const
because it modifies
the data members::
class Coordinate { public: Coordinate(); Coordinate(double x, double y); double getX() const; double getY() const; void read(istream & in); void display(ostream & out) const; private: double myX, myY; };
We can define read()
as a function member 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 ')' }
The ch
variable is used to read in the punctuation characters
— parentheses and the comma — and then simply "throw them away"
because they're not essential within a Coordinate
object —
every Coordinate
object is written with this
punctuation. We only need them when inputting a coordinate and in output.
Of course, we could have required that the user enter only the coordinates, but it's more user-friendly to have the input be in the customary form. With this function member, we can use statements like
to read aCoordinate point; point.read(cin);
Coordinate
of the form (x,y)
from
cin
.
Note that we didn't declare this function as inline. There are no
"hard and fast" rules for when to inline and when not. Inlining a function
that involves several operations can result in "code bloat." As a general
rule, functions that use more than a few — 3 or 4? —
should not be inlined.
Thus, given the length of this function member, we have opted not to
declare it as an inline function. In fact, some compilers may not allow it.
As a result, we put this definition in the implementation file
(without the keyword inline
).
Using this information:
Fraction Coding #5:
- Prototype and define an input function member named
read()
for classFraction
. Your function member 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()
ofCoordinate
:
- You can (and should) test the second half of the precondition (i.e.,
d
is not zero).
- Watch the punctuation. A coordinate has three punctuation characters, but a fraction has just one. Don't blindly copy the input statement. Change the comments so that it's clear what's being read in by each input operator.
- Test your input function member by uncommenting the section labeled
5. Test Input
infractionTester.cpp
Compile, link, and execute the program.- Continue to the next part of this lab exercise when
read()
is working correctly.
We have seen that function members 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
, aCoordinate
.
Return:result
, aCoordinate
.
Postcondition:result.myX
==myX
+point2.myX
andresult.myY
==myY
+point2.myY
.
Again, the postcondition here suggests the code that we should write.
But how do we write a function for +
? The answer comes
this property of C++:
Rule: For any overloadable operator Δ, we can use the notation operatorΔ
as the name of a function that overloads Δ with a new definition.
So, to overload +
for Coordinate
objects, we
define a function named operator+
; and we can make it a
function member of Coordinate
that will have the second
addend as a parameter.
class Coordinate { public: Coordinate(); Coordinate(double x, double y); double getX() const; double getY() const; void read(istream & in); void display(ostream & out) const; Coordinate operator+(const Coordinate & point2) const; private: double myX, myY; };
According to our specification, this operation does not modify the
data members of the Coordinate
that receives it, and so
is declared to be a const
function. But neither does it
modify the parameter, so that 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
faster than by value for non-primitive types because it does not
require making a copy of the argument.
One way to define this function member is as follows:
This definition uses our second constructor to construct and initializeCoordinate Coordinate::operator+(const Coordinate & point2) const { Coordinate result(myX + point2.getX(), myY + point2.getY()); return result; }
result
with the appropriate values.
Once such a function member 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 function member call:
point1.operator+(point2)
It is useful to overload other arithmetic operators for
Fraction
objects. We will describe here how to
do this for multiplication, and leave the others for the first project.
From the preceding discussion, it should be evident that
we need to overload operator*
so that the expression
oldAmount * scaleFactor
in fractionTester.cpp
can be used to multiply the two
Fraction
objects oldAmount
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 data members and then simplifying the resulting
Fraction
.
First, let's implement multiplication and forget about simplifying the result:
Fraction Coding #6:
- Extend your
Fraction
class with a definition ofoperator*
that can be used to multiply twoFraction
objects. When we add the code to simplifyresult
, this function member will be reasonably complicated, so define it in the implementation file (and, of course, prototype it in the header file, in the class declaration).
- Test the correctness of what you have written by uncommenting the code in the section labeled
6. Test multiplication
infractionTester.cpp
.
- Compile, link, and execute the resulting program. When your multiplication operation yields correct (but unsimplified) results, continue on with the following part of the lab exercise.
The main deficiency of our implementation of operator*
is its
inability to simplify fractions. That is, our
multiplication operation would be improved if class Fraction
had a simplify()
operation so 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
function member named simplify()
.
In a function member like operator*
which constructs its
answer in a Fraction
named result
, we can simplify
this answer by calling simplify()
as follows:
result.simplify();
Phrased in message-passing terms, this call
says, "Hey, result
! Simplify yourself!"
and 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 is with
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 almost writes itself.
The specification for this function member is thus:
Specification:
Postcondition:myNumerator
andmyDenominator
do not share any 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 receiving 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.
Fraction Coding #7:Using
greatestCommonDivisor()
and the preceding algorithm:
- Define
simplify()
as a function member of classFraction
. Because this is a complicated operation, define it in the implementation file and only prototype it in the header file.- Compile, link, and execute
fractionTester.cpp
to check that you multiplication operator is now working correctly and producing simplified results.
<<
and Input Operator >>
We have seen how arithmetic operators such as *
can be
overloaded. We turn now to the problem of overloading the
output operator <<
and input operator >>
.
While we have provided the capability to output a Fraction
value via a display()
function member, doing so requires that we write
clumsy code like:
cout << "\nThe converted measurement is: " newAmount.display(cout); cout << endl;instead of elegant code like:
cout << "\nThe converted measurement is: " << newAmount << endl
Our display()
function member 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 to use
the usual output 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 maybe we could add a function member 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 function member of Coordinate
, but C++ would then require us to invoke the function member in this way:
point << cout;But that looks like
cout
is being sent to point
— the exact opposite of what we want!
Instead, we can overload the output operator (<<
) as a
normal function (i.e., not as a function member) 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 "ordinary" (i.e., non-member) function in the header file, following the class declaration:
inline ostream & operator<<(ostream & out, const Coordinate & coord) { coord.display(out); return out; }
There are several subtle points in this definition that need further explanation:
inline
function. Since it is not a function member, defining it
in the header file serves as both prototype and definition for the
function. Inline function members must still be prototyped in the class
so that the compiler fully understands that they are members 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
.The leftmostcout <<value1
<<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 the way we want.
Coordinate
is done by sending the Coordinate
parameter coord
the display()
message we defined in Part I of this exercise,
with out
as its argument. If we hadn't written display()
, we could still define this function using the accessor
function members getNumerator()
and getDenominator()
.For our Fraction
class, the specification of this operation is thus:
Specification:
Receive:out
, anostream
, and aFraction
objectaFraction
.
Precondition:ostream out
is open for output.
Output:aFraction
, in the formn
/d
, viaout
.
Passback :out
, containingn
/d
.
Return:out
, for chaining.
Using this information:
Fraction Coding #8:
- Overload the output operator for class
Fraction
.- To test it, uncomment the section labeled
7. Test output operator <<
infractionTester.cpp
.- Compile, link, and execute the program. When it's working correctly, continue to the next section.
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:
There are a few differences between the input and output operators:inline istream & operator>>(istream & in, Coordinate & coord) { coord.read(in); return in; }
operator<<
; the input
operator is operator>>
.istream
instead of an ostream
.Coordinate
reference, rather than a
const
Coordinate
reference. Because 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:
Fraction Coding #9:
- overload the input operator
>>
for classFraction
, so that a user can enter3/4or (with spaces around the slash)
3 / 4to input the fraction 3/4.- To test your input operator, uncomment the section labeled
8. Test input operator >>
infractionTester.cpp
. Compile, link, and exeute the program and test whether your input operator works.- When everything in your
Fraction
class is correct, completeFraction.txt
.
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 data members. By default, C++ will not allow any other function to access private data members.
But suppose we had not written the read()
function member for class
Coordinate
, and wanted to overload operator>>
in
order to input Coordinate
values. We'd probably put 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 data members. As a non-function member,
this operator cannot access the private data members. 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 function members 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 function member 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 function memberology 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.