Next Page
A Second Constructor
A class can have multiple constructor functions,
so long as each definition is distinct in either the number or the
type of its arguments.
Defining the same function multiple times is called overloading
the function.
To illustrate, suppose that we would like to be able to explicityly
initialize the X-value and Y-value of a Coordinate object
to two values that are specified when the object is defined.
We might specify this task as follows:
Receive: xValue and yValue, two double values.
Postcondition: myX == xValue &&
myY == yValue.
We can perform this task by overloading the Coordinate constructor
with another definition that takes two doublearguments
and uses them to initialize our data members:
inline Coordinate::Coordinate(double xValue, double yValue)
{
myX = xValue;
myY = yValue;
}
As usual, such a simple function would be defined following class
Coordinate, in Coordinate.h,
and its prototype would be placed in the public section
of class Coordinate:
class Coordinate
{
public:
Coordinate();
Coordinate(double xValue, double yValue);
void Print(ostream & out) const;
private:
double myX,
myY;
};
Given such a function,
the C++ compiler will process a Coordinate declaration
statement like this:
Coordinate point1,
point2(1.2, 3.4);
by using our first constructor to initialize point1
(since it has no arguments), and use our second constructor to
initialize point2 (since it has two arguments),
resulting in objects that we might visualize as follows:
Using this information, define and prototype
a second Fraction constructor that satisfies this specification:
Receive: numerator and denominator, two integers.
Precondition: denominator != 0.
Postcondition: myNumerator == numerator &&
myDenominiator == denominator.
That is, the definitions:
Fraction oldMeasure;
...
Fraction scaleFactor(1, 6);
should initialize oldMeasure to 0/1,
and initialize scaleFactor to 1/6.
Use the C++ 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);
When your functions are working correctly, remove this "test code"
from pierre1.cpp.
Extractor Functions
Any operations we wish to perform on a class object can be prototyped
as public functions within the class.
For example, it might be useful to be able to extract the X-
and Y-values of a Coordinate object.
The first task can be specified as:
Return: my X-value.
while the second is
Return: my Y-value.
Since such a function member does not modify any of the class
data members, we write:
class Coordinate
{
public:
Coordinate();
Coordinate(double xValue, double yValue);
double X() const;
double Y() const;
void Print(ostream & out) const;
private:
double myX,
myY;
};
We would then define these simple functions in Coordinate.h,
as follows:
inline double Coordinate::X() const
{
return myX;
}
inline double Coordinate::Y() const
{
return myY;
}
Given such functions, if two Coordinate objects
point1 and point2 are as follows:
then the expression:
point1.X()
evaluates to 0.1, while the expression
point2.Y()
evaluates to 8.9.
We should mention that this is another reason for our convention of
prepending my to the names of the data members of a class
-- we can then use the names (without the my)
as the name of a function member that extracts the value of that data member.
Using this information, add to class Fraction
an extractor function member Numerator()
that satisifies this specification:
Return: myNumerator.
and an extractor function member Denominator() that satisifies this
specification:
Return: myDenominator.
Since these are simple functions, define them as inline
following the class declaration in Fraction.h.
Then test their syntax and continue when they are correct.
Input
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:
(3,4)
We can specify the problem as follows:
Receive: in, an ostream.
Precondition: in contains a Coordinate of the form (x,y).
Input: (x,y), from in.
Passback: in, with the input values extracted from it.
Postcondition: myX == x && myY == y.
Since the function modifies the data members of Coordinate,
it is not prototyped as a const function within the class:
class Coordinate
{
public:
Coordinate(void);
Coordinate(double xValue, double yValue);
double X() const;
double Y() const;
void Read(istream & in);
void Print(ostream & out) const;
private:
double myX,
myY;
};
To solve the problem, 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-value
>> ch // read ','
>> myY // read Y-value
>> ch; // read ')'
}
With six operations, this function is pressing the boundaries
of how some compilers define "simple."
As a result, we would define it in Coordinate.cpp
(without the keyword inline),
instead of in Coordinate.h as an inline function.
Given such a function, the statements:
Coordinate point;
point.Read(cin);
would read a Coordinate of the form (x,y) from cin.
Using this information, define and prototype an input function named
Read() for class Fraction.
Your function should satisify this specification:
Receive: in, an istream.
Precondition: in contains a Fraction value of the form n/d,
such that d != 0.
Input: n/a, from in.
Passback: in, with Fraction n/d extracted from it.
Postcondition: myNumerator == n &&
myDenominator == d.
That is, you should be able to "uncomment" the statements:
oldMeasure.Read(cin);
...
scaleFactor.Read(cin);
and read a Fraction value from cin
into oldMeasure and scaleFactor.
Put differently, we should be able to send a Fraction
object the Read() messages, with the istream
from which it should read as an argument.
Test your input function by adding statements like those above to
pierre1.cpp, along with a Print() that echos
the input values back to the screen.
Compile and run the program, and continue when Read()
works correctly.
Fractional Multiplication
We have seen that functions 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.
As an illustration, suppose that we want to permit
two Coordinate objects to be added together.
In the object-oriented world, an expression like
point1 + point2
is thought of as sending the + message to point1,
with point2 as a message argument.
That is, we can specify the problem from the perspective
of the Coordinate receiving this message as follows:
Receive: point2, a Coordinate.
Return: result, a Coordinate.
Postcondition: result.myX == myX + point2.myX &&
result.myY == myY + point2.myY.
According to our specification, this operation
does not modify the data members of the Coordinate
that receives it, and so we write the following prototype:
class Coordinate
{
public:
Coordinate(void);
Coordinate(double xValue, double yValue);
double X() const;
double Y() const;
void Read(istream & in);
void Print(ostream & out) const;
Coordinate operator+(const Coordinate & point2) const;
private:
double myX,
myY;
};
One way to define this function is as follows:
Coordinate Coordinate::operator+(const Coordinate & point2) const
{
Coordinate result(myX + point2.X(), myY + point2.Y());
return result;
}
This definition uses our second constructor function to construct
and initialize result with the appropriate values.
This function illustrates that, for any overloadable operator
D,
we can use the notation
operatorD
as the name of a function that overloads D
with a new definition.
Once such a function has been prototyped and defined as a member of
class Coordinate, we can write normal looking expressions, such as
point1 + point2
to compute the sum of two Coordinate objects
point1 and point2.
The C++ compiler treats such an expression as
an alternative notation for the function 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 * scaleFactor
can be used to multiply the two Fraction objects
oldMeasure and scaleFactor.
We can get some insight into the problem by working some simple examples:
1/2 * 2/3 = 2/6 = 1/3 3/4 * 2/3 = 6/12 = 1/2
The specification for such an operation can be written as follows:
Receive: rightOperand, a Fraction operand.
Return: result, a Fraction, containing the product of
the receiver of this message and rightOperand,
simplified, if necessary.
From these examples, it should be apparent that
we can construct result by taking the product
of the corresponding data members
and then simplifying the resulting Fraction.
For the moment, let's ignore the problem of simplifying an
improper 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 function
will be reasonably complicated, so define it in Fraction.cpp.
(Don't forget the prototype in Fraction.h!)
Then 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.
Fraction Simplification.
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 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
member function named Simplify(),
such that a function like operator* can call
Fraction Fraction::operator*(const Fraction & right) const
{
// compute result...
result.Simplify();
return result;
}
in order to reduce a Fraction object's value.
There are a number of ways to simplify a fraction.
One straightforward way is the following algorithm:
a. Find gcd, the greatest common divisor of
myNumerator and myDenominator.
b. Replace myNumerator by myNumerator/gcd.
c. Replace myDenominator by myDenominator/gdc.
The implementation file Fraction.cpp
contains a function GreatestCommonDivisor()
that implements Euclid's algorithm for finding the greatest
common divisor of two integers.
Using function GreatestCommonDivisor()
and the preceding algorithm, define function Simplify()
as a function member of class Fraction.
Since this is a complicated operation, define it in Fraction.cpp,
rather than in Fraction.h.
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 class.
Part II. pierre2.cpp
In this second part of today's 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.
Output Revisited
While we have provided the capability to output a Fraction value
via a Print() function member, 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";
Put differently, our Print() function member solves the problem,
but it 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.
To see how to do so, let's revisit our Coordinate class.
What we would like to be able to do is write:
cout << point << endl;
to display a Coordinate object named point.
One way to do this would be to 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 << point
as the call
cout.operator<<(point)
However, this would require us to modify a predefined, standardized class.
This is never a good idea, since the resulting class will no
longer be standardized.
Instead, we can overload the insertion operator (<<)
as a normal (i.e., non-member) function that
takes an ostream (e.g., cout) and a Coordinate
(e.g., point) as its operands.
That is, an expression
cout << point
will be translated by the compiler as a "normal" function call
operator<<(cout, point)
instead of as a message being sent to an object of some class.
To do this, we would define the following function in Coordinate.h,
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:
-
Because the function is simple, we would define it in
Coordinate.h as an inline function.
Since it is not a function member, defining it in the header file
serves as both prototype and definition for the function.
-
In an output expression of the form:
cout << Value ;
we see that an output operator takes two operands.
The left operand is cout, an ostream,
which is altered by the operation
(i.e., Value gets inserted into it),
and so an ostream reference parameter must be declared
for this operand.
The right operand is Value
(of whatever type is being displayed).
Since we are overloading operator<< for a Coordinate,
and the second parameter is received but not passed back,
this second parameter is declared as a constant Coordinate reference,
to avoid the copying overhead associated with a value parameter.
-
In an output statement of the form:
cout << Value1 << Value2 << ... << ValueN;
we see that output expressions can be chained together.
That is, the leftmost << 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.
Our function must thus return an ostream to its caller,
if such chaining is to work correctly.
However, if we simple make the return-type ostream:
inline ostream operator<<(ostream & out, const Coordinate & coord)
{
coord.Print(out);
return out;
}
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:
inline ostream & operator<<(ostream & out, const Coordinate & coord)
{
coord.Print(out);
return out;
}
Given such a definition, the insertion operators in the statement
cout << "\nThe converted measurement is: " << newMeasure << "\n\n";
will each get cout as their left operand.
-
The actual work of formatting and outputting the
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 extractor functions Numerator()
and Denominator().)
For our Fraction class, the specification of this operation is thus:
Receive: out, an ostream,
aFraction, a Fraction.
Precondition: aFraction.Numerator() == n &&
aFraction.Denominator() == d.
Output: aFraction, in the form n/d, via out.
Passback: out, containing n/d.
Return: out, for chaining.
Using this information, overload the output operator
for class Fraction, so that if the value of newMeasure
is 1/2 , then
cout << "\nThe converted measurement is: " << newMeasure << "\n\n";
will cause
The converted measurement is: 1/2
to be displayed on the screen.
Test the syntax of what you have written (using make -k pierre2)
and continue when it is correct.
Input Revisited
Now that we've seen how to do output, input is easy.
To illustrate, if we wanted to input a Coordinate, entered as
(3,4)
then we could define:
inline istream & operator>>(istream & in, Coordinate & coord)
{
coord.Read(in);
return in;
}
From this, we can see that the primary differences between the
insertion and extraction operators are:
-
The insertion operator is
operator<<,
the extraction operator is operator>>.
-
The left operand of the extraction operator
(i.e.,
cin) is an istream
instead of an ostream.
-
The right operand is a
Coordinate reference,
rather than a constant Coordinate reference.
-
The function returns a reference to an
istream
(i.e., its left operand) instead of an ostream,
to permit input expressions to be chained together
in an input statement.
As with the output operator, this operation is sufficiently simple
to define as inline within Coordinate.h.
Using this information, overload the extraction operator for class
Fraction, so that a user can enter
3/4
to input the fraction 3/4.
Then "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,
using copy-and-paste techniques to complete Fraction.doc.
Friend Functions
While it is not necessary for this particular problem,
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.
To illustrate, suppose we had not written the Read()
function member for class Coordinate, and wanted to
overload operator>> in order to input Coordinate values.
As we saw earlier, we should not overload operator>>
as a function member of class istream, since doing so
would mean that istream was no longer a standard class.
That drives us to overload operator>> as a "normal"
function -- one that is not a member of any class.
If we do so as follows:
istream & operator>>(istream & in, Coordinate & coord)
{
char ch;
in >> ch // consume (
>> coord.myX // read X-value
>> ch // consume ,
>> coord.myY // read Y-value
>> ch; // consume )
}
then the compiler will generate an error when we compile,
because as a non-member function, operator>>
is not permitted to directly access the private data members
myX and myY of class Coordinate.
Here is a situation where a non-member function needs to
be granted access to the private section of a class.
For such situations, C++ provides the friend mechanism.
If a class names a function as a friend,
then that non-member function is permitted to access the private
section of the class.
A class names a function as a friend by including a prototype
of the function preceded by the keyword friend.
Thus, we would have to write this in our Coodinate class:
class Coordinate
{
public:
Coordinate(void);
Coordinate(double xValue, double yValue);
double X() const;
double Y() 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:
-
It tells the compiler that the function is not
a member of the class; and
-
It tells the compiler that a definition of that function
is nevertheless permitted to access the private section
of the class.
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 of a class.
Object-Centered Design Revisited
Now that you have seen how to build a class,
we need to expand our design methodology to incorporate classes:
1. Describe the behavior of the program.
2. Identify the objects in the problem:
If an object cannot be directly represented using available types:
Design and implement a class to represent such objects.
3. Identify the operations needed to solve the problem:
If an operation is not predefined:
1) Design and implement a function to perform that operation.
2) If the operation is to be applied to a class object:
Design and implement the function as a function member
(or a friend).
4. Organize the objects and operations into an algorithm.
Using this methodology and the C++ class mechanism,
we can now create a software model of any object!
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!
Phrases you should now understand:
Class, Data Member, Function Member, Scope Operator,
Precondition, Postcondition,
Constructor, Overloading, Operator Overloading, Friend Function.
Submit:
Hard copies of your final version of Fraction.h,
Fraction.doc,
Fraction.cpp, pierre1.cpp, pierre2.cpp,
and an execution record showing the execution of each program.
|
|
|
|
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 |