Lab 12: Building Classes


Introduction

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.

Files

Download these files and add your name and other information required by your instructor to the opening documentation of the code and documentation files.

Creating Classes

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:

  1. data members (also known as instance variables) that store the items that make up an object of this new type and describe its attributes; and
  2. function members (also called methods) that provide its operations.

For example, for a Temperature class we might have

These two kinds of members of a class are usually organized into two sections in a class declaration of the form

class TypeName
{
  public:
   // -- Declarations of function members
  private:
   // -- Declarations of data members
};
Such a declaration is (almost always) saved in a header file named 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 #includes 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:

class TypeName
{
  public:

  private:
    Type1 DataMemberList1;
    Type2 DataMemberList2;
    ...
    Typen DataMemberListn;
};
where each 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/2
4/3
4/16
16/4
Each fraction has the form:
number1 / number2
where 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 file Fraction.h and do the following: Look at the patterns and example above if you need help.

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 program fractionTester.cpp, uncomment the declaration of the Fraction object f:

    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.

Function Members

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.)

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 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.

Function Members As Messages

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 strings.

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.

Adding Operations to the Fraction Class — Part I

Based on what we've just discussed about function members sending messages, we might expect that defining them will be somewhat different from defining a normal function. The remainder of this lab exercise will describe how this is done, illustrating with our Coordinate 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.

An Output Function Member

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:
      Receive: out, an ostream to which I write my values.
      Output: myX and myY, as an ordered pair.
      Passback: out, containing the output values.
Note the use of internal perspective — e.g., "... I write my value." — used because we are defining a built-in operation.

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 << ')';
}

Now try the following to check your understanding of this rather lengthy presentation.

Question #12.2: If point is a Coordinate object with x-coordinate 3 and y-coordinate 4, what output (if any) will be produced by the statement point.display(cout);

Question #12.3: If origin is a Coordinate object with x-coordinate 0 and y-coordinate 0, what output (if any) will be produced by the statement point.display(cerr);

Using the preceding as a guideline:

Fraction Coding #1:

Constructors

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

Coordinate point;
the compiler will call a constructor function to initialize the data members of that object — 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 and myY == 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.

Rule: The name of a constructor is always the name of the class.
  It has no return type — not even void.
And because it is a member of a class, its prototype must appear within the class. So, for our 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:

inline Coordinate::Coordinate()
{
  myX = 0.0;
  myY = 0.0;
}
The first 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:

ClassName::ClassName(ParameterList)
{
  StatementList
}
where the first 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:

A Second Constructor

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 and y, two double values.
      Postcondition: myX == x and myY == 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:

class Coordinate
{
  public:
    Coordinate();
    Coordinate(double x, double y);
    void display(ostream & out) const;
  private:
    double myX, myY;
};
We can now declare Coordinate objects and initialize them by using this initial-value constructor; for example,
Coordinate point1,
           point2(1.2, 3.4);
The default-value constructor initializes 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:

Input

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, an istream.
      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 and myY == 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

Coordinate point;
point.read(cin);
to read a 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:

Fractional Multiplication

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 + point2
is thought of as sending the + message to point1, with point2 as a message argument. We can specify the problem from the perspective of the Coordinate receiving this message:
Specification:
      Receive: point2, a Coordinate.
      Return: result, a Coordinate.
      Postcondition: result.myX == myX + point2.myX and result.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:

Coordinate Coordinate::operator+(const Coordinate & point2) const
{
  Coordinate result(myX + point2.getX(), myY + point2.getY());
  return result;
}
This definition uses our second constructor to construct and initialize result with the appropriate values.

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 + 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 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:

In the first example, the result is obtained by multiplying the numerators together and the denominators together. In the last two examples, however, we need to simplify this result by reducing it to lowest terms.

The specification for such an operation can be written as follows:

Specification:
      Receive: rightOperand, a Fraction operand.
      Return: result, a Fraction, containing the product of the receiver of this message and rightOperand, 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:

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:

  1. Find gcd, the greatest common divisor of myNumerator and myDenominator.
  2. Replace myNumerator by myNumerator/gcd.
  3. Replace 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 and myDenominator 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:


Part II. Overloading Output Operator << 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 >>.

Output Revisited

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 << point
as 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 << point
will 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:

For our Fraction class, the specification of this operation is thus:

Specification:
      Receive: out, an ostream, and a Fraction object aFraction.
      Precondition: ostream out is open for output.
      Output: aFraction, in the form n / d, via out.
      Passback : out, containing n / d.
Return: out, for chaining.

Using this information:

Fraction Coding #8:

Input Revisited

As a dual to output, all of the things we learned about the output operator also apply to the input operator, just with a slight change in direction (reading information in, instead of sending it out). The syntax is very similar.

Suppose we wanted to input a Coordinate, entered as

  (3,4)
We could define this operator in the header file:
inline istream & operator>>(istream & in, Coordinate & coord)
{
  coord.read(in);
  return in;
}
There are a few differences between the input and output operators:

This operation is sufficiently simple to inline it within the header file.

Using this information:

Fraction Coding #9:

Part III: Friend Functions

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.

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 an object.
  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 function).
  4. Organize the objects and operations into an algorithm.

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.


Lab Home Page


Report errors to Larry Nyhoff (nyhl@cs.calvin.edu)