Lab 10: Building Classes


Introduction

Most of the problems we have examined have had relatively simple solutions, because the data objects in the problem could be represented using the predefined C++ types. For example, we can represent a menu with a string, a choice from that menu with a char, the radius of a circle with a double, and so on.

The problem is that real-world problems often involve data objects that cannot be directly represented using the predefined C++ types. For example, suppose that we know a certain gourmet chef named Pièrre whose recipes are written to make 12 servings. The difficulty is

  1. Pièrre frequently must prepare a dish for numbers of customers different than 12 (e.g., 1 customer = 1/12 of a recipe, 2 customers = 1/6 of a recipe, 15 customers = 15/12 = 5/4, and so on.)
  2. Pièrre's recipes are written using fractions (e.g., 1/2 tsp., 3/4 cup, etc.) so that he must multiply fractions to reduce the size of a recipe, and
  3. Pièrre is so poor at multiplying fractions, that he has hired us to write a program that will enable him to conveniently multiply two fractions.
We have provided two programs for today's exercise: pierre1.cpp and pierre2.cpp. In the first part of the exercise, we will do what is needed to get pierre1.cpp operational, and in the second part of the exercise, we will extend the first part so that pierre2.cpp is operational.

Getting Started

Begin by making a new directory in which to store your work for this exercise. Then save copies of the files pierre1.cpp, pierre2.cpp, Fraction.h, Fraction.cpp, Fraction.doc, and Makefile in this directory. Take a moment to compare the programs in pierre1.cpp and pierre2.cpp. Each program implements the same basic algorithm:

   1. Get oldMeasure, the fractional measure to be converted.
   2. Get scaleFactor, the fractional conversion factor.
   3. Compute newMeasure = oldMeasure * scaleFactor.
   4. Output newMeasure.
Note that a solution to Pièrre's problem is quite simple, given the ability to define, input, multiply and output fraction objects. The two programs differ only in how they perform input and output Fraction objects. Note also that some lines are "commented out" at present. We will be "uncommenting" these lines as we develop the functionality needed in order for such lines to work properly.

The difficulty is that there is no predefined C++ type Fraction by which such objects can be defined or operated upon. In such situations, C++ provides a mechanism by which a programmer can create a new type and its operations. This mechanism is called the class, which is the subject of today's exercise.

Creating Classes

In C++, a new type can be created by

  1. Defining the data objects that make up the attributes of an object of the new type (i.e., the things of which an object of that type consists); and
  2. Surrounding those definitions with a class structure, which has the form:
       class TypeName
       {
        public:
    
        private:
    
       }; 
    where TypeName is a name describing the new type.
As indicated, a class has two sections, a public section and a private section. The public section is where class operations are declared, and the private section is where class attributes are declared.

To illustrate, suppose that we want to define a new type whose objects can be used to store Cartesian coordinates. Such an object has two attributes: an X-value, and a Y-value, both reals. We can define data objects for these attributes as follows:

   double myX,
          myY;
(To distinguish attribute identifiers from other identifiers, we will place the word my at the beginning of a data member's name.) We then surround them with an appropriately-named class structure:
   class Coordinate 
   {
    public:

    private:
      double myX,
             myY;
   }; 
The result is a new type, named Coordinate, which can be used to declare data objects, such as a point:
   Coordinate point;
The object point then has two real components, one named myX and the other named myY.

In general, the data portion of a class definition can be thought of as following this pattern:

   class TypeName
   {
    public:

    private:
      Type1 AttributeList1 ;
      Type2 AttributeList2;
      ...
      TypeN AttributeListN ;
   };
where each Typei is any defined type; and each AttributeListi is a list of the Typei attributes of an object of type TypeName.

Now, if we apply this approach to the problem we are trying to solve, we see that we need to identify the attributes of a Fraction object. If we examine a few fractions

   1/2     4/3   4/16  16/4
we can see that each fraction has the form:
   number1/number2
where number1 is called the numerator and number2 is called the denominator. The numerator and denominator are different from one fraction to another, and so these quantities must be recorded for any given fraction value; however the / symbol is common to all fractions, and so it need not be recorded as an attribute. A fraction thus has two attributes, its numerator and its denominator, both of which are integers.

Begin editing the file Fraction.h, and define two integer data objects named myNumerator and myDenominator to represent these two attributes. (Yes, you should prepend my to the beginning of each name, for reasons that will be apparent shortly). Then surround these definitions with a class structure that declares the name Fraction as a new type whose objects contain these two attributes. Be sure to arrange your class so that myNumerator and myDenominator are within the class, but following the keyword private:.

Given this declaration of Fraction, object declarations like this

   Fraction oldMeasure;
   ...
   Fraction scaleFactor;
can be thought of as defining two data objects with the following forms:

The objects within a class object that store its attributes are usually called the data members of that object.

Note that each object of type Fraction has its own copy of each of the attributes we defined. This is why we preface the names of these attributes with my, to indicate that from the perspective of the object, attributes are characteristics of that object. That is, within the Fraction object named oldMeasure, myNumerator refers to its first data member myDenominator refers to its second data member. However, within the object named scaleFactor, myNumerator refers to its first data member myDenominator refers to its second data member. Each object thus has its own separate data members.

Since a class may contain an arbitrary number of attributes, a software model can be constructed for virtually any real-world object, simply by defining data objects for each of its attributes, and then surrounding those data objects with an appropriately-named class structure.

In the source program pierre1.cpp, the definition of oldMeasure is currently commented out. Modify your source program so that this declaration is no longer commented out (but the subsequent lines are). Then compile your source program, to test the syntax of what you have written. When it is correct, continue to the next part of the exercise.

Function Members

Besides having data members, a class can also have functions which are called the function members (or member functions) of the class. Function members provide a means by which the operations on a class object can be encoded.

Function members must be prototyped within the class structure itself; and are normally defined in the class implemention file. However, very simple function members (i.e., that fit on a single line) are by convention defined in the header file, following the class declaration, and prefixed by the keyword inline. (If we designate a function as inline, we are suggesting to the compiler that it replace calls to the function with the actual body of the function, substituting arguments for parameters as appropriate. This should only be done in the header file, not in the implementation file.)

Class Structure and Information Hiding

One of the characteristics of a class is that its data members are kept private, meaning that a program using the class is not permitted to directly access them. (This is a good idea because a program that directly accesses the data members of a class becomes dependent on those particular data members. If those data members are changed (which is not uncommon in class maintenance), then such programs must also be changed, increasing the cost of software maintenance.)

While it is a good idea for the data members of a class to be kept private, we want users of the class to be able to perform operations on class objects. As a result, the operations should be declared as public members, in contrast to the data members.

This is the reason for the keywords public: and private: within the class: the space between public: and private: defines a public section where function members can be declared, and the space between private: and the end of the class defines a private section where the data members can be declared. The pattern is thus as follows:

   class TypeName
   {
    public:
	// Class Operation Declarations - public!
    private:   
	// Class Attribute Declarations - private!
   };
All of the declarations that follow the keyword public: can be accessed by programs using the class; and all of the declarations that follow the keyword private: cannot be accessed by programs using the class.

By convention, the "public section" of a class comes first, so that a user of the class can quickly see what operations it provides. It is good style to have one "public part" and one "private part" in a class; however the keywords public: (and private:) can appear an arbitrary number of times in a class declaration, if multiple public sections are needed.

Function Members As Messages

We have seen that function members are called differently from "normal" functions: if two string objects named greeting and closing are defined as follows:

   string greeting = "hello",
          closing = "goodbye";
then the expression
   greeting.size()
returns the value 5, while the expression
   closing.size()
returns the value 7.

Object-oriented programmers like to think of a call to a function member as a message to which the object responds. To illustrate, when we send the size() message to greeting, greeting responds with the number of characters it contains (5); and if we send the same size() message to closing, closing responds with the number of characters it contains (7). The effect of this approach is to shift the point of view from the function to the object.

Defining a function member is thus a bit different from defining a "normal" function, because the definition of a function member describes what a class object does when it receives that message. Put differently, function members are usually written from the perspective of the class object.

Part I. pierre1.cpp

In this first part of today's exercise, we will focus on adding the function members to class Fraction needed to get pierre1.cpp operational.

An Output Function

To facilitate debugging a class, it is often a good idea to begin with a function member that can be used to display class objects -- an output function.

From the perspective of our Coordinate class, we can specify the task of such a function as follows:

   Receive: out, an ostream to which I am to write my values.
   Output: myX and myY, as a pair.
   Passback: out, containing the output values.
Function members must be prototyped within the class declaration, so if we were to name our function Print(), we would write:
   class Coordinate 
   {
    public:
      void Print(ostream & out) const;
    private:
      double myX,
             myY;
   };
Function members that do not modify the class data members are declared as const function members, by placing the keyword const after the function's parameter list, as shown.

As it happens, this is a fairly simple function, so we would define it within the header file Coordinate.h, following the declaration of class Coordinate, and precede its definition with the keyword inline.

To define Print(), we must inform the compiler that this is a function member, as opposed to a "normal" function. This is done by preceding the name of the function with (i) the name of the class of which the function is a member; and (ii) the scope operator (::). That is, we would define Print() as a function member of our Coordinate class as follows:

   inline void Coordinate::Print(ostream & out) const
   {
      out << '(' << myX << ',' << myY << ')';
   }
As explained earlier Note that const must be present in both the prototype and the definition of a function member that does not modify its function members.

Note also that the prefix my helps to reinforce the notion that this is a message to which a Coordinate object responds. That is, if point is a Coordinate object whose X-value is 3 and whose Y-value is 4, then the statement

   point.Print(cout);
displays via cout:
   (3,4)
Similarly ,if origin is a Coordinate object whose X-value is 0 and whose Y-value is 0, then the statement
   origin.Print(cerr);
displays via cerr:
   (0,0)
Note that as a message to an object, a function member must be invoked using dot notation, which specifies the object to which the message is being sent.

Note finally that as a message to which an object responds, a function member can directly access the private data members of the object. (If we were to omit the Coordinate:: in the definition, a compilation error would result.)

Using this information as a pattern, prototype and define a similar Print() function for your Fraction class, such that if oldMeasure is a Fraction whose numerator is 3 and whose denominator is 4, then a message:

   oldMeasure.Print(cout);
will display
   3/4
Prototype this function in the public section of class Fraction, and define it as an inline function following the declaration of class Fraction, in the Fraction.h. Check the syntax of your function, and continue when it is correct.

The Class Constructor

An output operation for a class is of little use unless we are able to define and initialize objects of that class. The action of defining and initializing an object is called constructing that object. To allow the designer of a class to control the construction of class objects, C++ allows us to define a function called a class constructor function that specifies exactly what actions are to be taken when a class object is constructed. When a class object is defined, the C++ compiler calls this function to initialize the object's data members.

For example, suppose we would like for a definition of a Coordinate object:

   Coordinate point;
to initialize the data members of point to zeros. We might specify this task as follows:
   Postcondition: myX == 0.0 && myY == 0.0.
Note that a constructor function does not return anything to its caller, it simply initializes the data members of an object when that object is defined. We specify this behavior through a boolean expression that is true when the function terminates. Such an expression is called a postcondition, since it is a condition that holds when execution reaches the end of the function.

In order for a function to be a function member of a class, its prototype must appear within the class, and the name of a constructor function is always the name of the class, so we declare this function in the public section of class Coordinate, as follows:

   class Coordinate
   {
    public:
      Coordinate();
      void Print(ostream & out) const;
    private:
      double myX,
             myY;
   };
Note that unlike our Print() function, a constructor initializes (i.e., modifies) the data members of a class. As a result, it must not be prototyped or defined as a const function member.

As we said earlier, a function member definition must have the name of the function preceded by the name of the class and the scope operator (::), and simple definitions should be placed in the class header file, designated as inline functions. To define a Coordinate constructor function, we thus write this funny-looking definition:

   inline Coordinate::Coordinate()
   {
      myX = 0.0;
      myY = 0.0;
   }
The first Coordinate is the name of the class, telling the compiler that this is a function member of class Coordinate. The second Coordinate is the name of the function, telling the compiler that this is a class constructor function for class Coordinate.

Given this function member, when a Coordinate object is defined, the C++ compiler will automatically call this function to initialize this object, which sets the object's myX and myY members to zero values.

Note that a constructor has no return type (not even void). As was mentioned earlier, this is because a constructor never returns anything to its caller -- it merely initializes objects of its class.

The pattern for a constructor function definition is thus:

   ClassName::ClassName(ParameterList)
   {
      StatementList
   }
where the first ClassName refers to the name of the class, the second ClassName names the constructor function, and StatementList is a sequence of statements that initialize the data members of the class. Constructors can take parameters, which are defined as they would be for any other function, and any valid C++ statement can appear in the body of such a function.

Using this information, prototype and define a constructor for your Fraction class, that satisifies the following specification:

   Postcondition: myNumerator == 0 && myDenominator == 1.
That is, the definition:
   Fraction oldMeasure;
should initialize the data members of oldMeasure appropriately to represent the fraction 0/1.

Store the prototype in the public section of class Fraction, and define it as inline, following the declaration of class Fraction, in Fraction.h. Test the syntax of what you have written, and continue when it is correct.

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/gcd.
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:
  1. 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.
  2. 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.

  3. 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.
  4. 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: 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: 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.


Back to This Lab's Home Page

Back to the Prelab Questions

Forward to the Homework Projects


Copyright 1998 by Joel C. Adams. All rights reserved.