Lab 11: 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. We can represent a menu with a string, a choice from that menu with a char, the radius of a circle with a double, and so on.

The problem is that real-world problems often involve data objects that cannot be directly represented using just a single, predefined C++ type.

Let's consider two different problems.

Problem #1: Cartesian points. In the algebra courses you've taken, you have probably graphed some functions on a two-dimensional graph. These graphs typically use a Cartesian coordinate system, with x- and y-coordinates, to plot the points.

This graph plots the point (7,3), which is 7 units to the right along the x-axis and 3 units up along the y-axis. In this lab, we'll only worry about representing a single point. This work, though, could easily be used in a program to generate some actual graphs in a Cartesian coordinate system.

Problem #2: Fractions. The other problem is doing fractional arithmetic. Suppose that we know a certain gourmet chef named Pièrre whose recipes are written to make 12 servings. There a few difficulties:

  1. Pièrre frequently must prepare a dish for more or fewer than 12 customers, requiring the scaling of his recipes (e.g., 1 customer results in 1/12 of a recipe, 2 customers results in 1/6 of a recipe, 15 customers results in 15/12 = 5/4 of a recipe, etc.).
  2. Pièrre's recipes are written using fractions (e.g., 1/2 tsp., 3/4 cup, etc.) so that he must multiply fractions when scaling a recipe.
  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 will work through two programs to manipulate fractions.

Coding. Keep in mind that you will write the code for the Fraction class. The Coordinate class is presented here as an example to help you write code for the Fraction class.

Files

Directory: lab11

Create the specified directory, and copy the files above into the new directory. Only gcc users need a makefile; all others should create a project and add all of the .cpp files to it.

Add your name, date, and purpose to the opening documentation of the code and documentation files; if you're modifying and adding to the code written by someone else, add your data as part of the file's modification history.

Looking at the Code

Take a moment to compare the programs in pierre1.cpp and pierre2.cpp. Both programs implement 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.

A solution to Pièrre's problem is quite simple, if we have the ability to define, input, multiply and output fraction objects. That's the work we'll be doing in this lab.

The two programs differ only in how they read and write fraction objects. The second version has a more familiar look to it, but will require more work from us. So the first version will be useful for getting something workable, but it's missing some input and output features.

Creating Classes

The difficulty is that there is no predefined C++ type Fraction. In such very common situations, C++ provides a mechanism by which a programmer can create a new type and its operations. This mechanism is called a class.

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

  1. Defining the data objects, known as instance variables, that make up the attributes of an object of the new type.
  2. Surrounding those definitions with a class structure.

The class structure mentioned in this second step looks like this:

class TypeName
{
  public:

  private:

};
where TypeName is a name describing the new type. A class is (almost) always defined in a header file.

A class has two sections, a public section and a private section. The public section is where class operations are declared, and the private section is where class attributes are declared.

Let's first consider a point in Cartesian coordinates. A point consists of an x-coordinate and a y-coordinate. We can easily write two variables to hold these two values:

double myX, myY;

Of course, the context for this declaration is all important. We declare these variables in an appropriately-named class structure, making them instance variables:

class Coordinate
{
  public:

  private:
    double myX, myY;
};

We use the prefix "my" at the beginning of a name for an instance variable name to encourage an internal perspective. We imagine ourselves as the object we're trying to describe. For example, when figuring out these instance variables, I could say, "I am a Cartesian coordinate, and I have an x-coordinate and a y-coordinate." Since they are my attributes, I label them as such. This way I shouldn't get them confused with other objects that I work with.

The result is a new type, named Coordinate, which can be used as a type for new objects:

Coordinate point1, point2, point3, point4;

Each pointI has two double components, one named myX and the other named myY. This is why myX and myY are known as instance variables: every instance of a Coordinate has its own copies of these variables. The neat thing is that we only had to declare myX and myY once in the class definition.

We can now expand our general pattern for a class definition:

class TypeName
{
  public:

  private:
    Type1 InstVarList1;
    Type2 InstVarList2;
    ...
    TypeN InstVarListN;
};
where each TypeI is any defined type and each InstVarListI is a list of instance variables of type TypeI.

Now let's apply this same thinking to the fraction problem. We first need to identify the attributes of a Fraction object. Here are a few fractions:

1/2
4/3
4/16
16/4
Each fraction has the form:
number1/number2
where number1 is called the numerator and number2 is called the denominator. A fraction needs both a numerator and a denominator. However, the / symbol is common to all fractions, and so it is not recorded as an attribute of a fraction. Consequently, a fraction has just two attributes, a numerator and a denominator, both of which are integers.

Edit the file Fraction.h:

Look at the patterns and example above for help with these two steps.

Given this declaration of Fraction, we can make the following declarations:

Fraction oldMeasure;
...
Fraction scaleFactor;
These declarations define two objects with the following forms:

Again, note that each instance of type Fraction has its own copy of each of the attributes we defined; that's why they're called instance variables. It's also why we prefix the variable name with "my", to encourage an internal perspective.

In the source program pierre1.cpp, uncomment the definition of oldMeasure. (Do not uncomment anything else, yet.) Compile your source program to test the syntax of what you have written. When it is correct, continue to the next part of the exercise.

Methods

Besides having instance variables, a class can also have methods. A method is a function declared inside a class to provide an operation for objects of the class.

We've used several methods already. A vector has a size() method. A string has a substr() method. But until now we've only used methods. Now we get to define our own methods in our own classes.

Methods are prototyped within the class structure itself (in the header file), and they are normally defined in the class implementation file. However, very simple methods (i.e., ones that fit on a single line) are defined "inline": in the header file, following the class declaration, prefixed by the keyword inline. The reasons for this are a bit technical and beyond the scope of what we're really trying to do. Just remember to define simple methods in the header file with an inline in front of them.

Class Structure and Information Hiding

One of the characteristics of a class is that its instance variables are kept private, meaning that a program using the class is not permitted to directly access them. Mostly this is an issue of trust. In the case of a Coordinate object, it probably wouldn't be too bad if other programmers had direct access to the myX and myY instance variables. However, in the Fraction class, we must make sure that myDenominator does not become zero since division by 0 is undefined. If we hide away myDenominator from the casual programmer, we can write the methods of the Fraction class so that myDenominator never becomes 0.

In practice, it is not worth the time and effort to try to figure out which instance variables are safe to let everyone access directly. Common practice is to make all instance variables private.

On the other hand, we do want users of the class to be able to access the operations of a class. As a result, the operations should be declared in the public section of the class. Anything defined in the public section can be accessed through an instance of the class from any part of the program. So, wherever we have a Fraction object, we will be able to access its public operations.

By convention, the public section of a class comes first, so that a user of the class can quickly see what operations it provides. It is good style to have one public section and one private section in a class; however, C++ allows the keywords public: and private: to appear an arbitrary number of times in a class declaration.

Methods As Messages

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

string greeting = "hello",
       closing = "goodbye";
then the expression
cout << greeting.size() << ' ' << closing.size() << endl;
prints 5 7, the sizes of the two strings.

Object-oriented programmers like to think of a call to a method as a message to which the object responds. When we send the size() message to greeting, greeting responds with the number of characters it contains; and if we send the same size() message to closing, closing responds with the number of characters it contains. This is part of the internal perspective for designing and writing a class. When we use the string class, we're not responsible for its operations. We just simple ask the string objects to carry out various actions.

As you might expect, defining a method is also a bit different from defining a normal function.

Part I: pierre1.cpp

In this first part of this exercise, we will focus on adding the methods to class Fraction needed to get pierre1.cpp operational.

An Output Method

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

From the perspective of our Coordinate class, we can specify the task of such a method, which we'll call print(), as follows:

Specification:
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.").

Also note that this method receives and passes back an ostream. The rule is very simple:

Rule: All streams are passed by reference---received and passed back.

The reasons for this are fairly technical and are beyond what we really need to know right now, but this is hard-and-fast rule.

Methods must be prototyped within the class declaration, so we would write:

class Coordinate
{
  public:
    void print(ostream & out) const;
  private:
    double myX, myY;
};

Whoa! Where did that const come from? Internal perspective is an easy perspective for explaining this: "I am a Coordinate object, and when I am printed using print(), I should not change." It's the const that adds the "I should not change". Think about it: a Coordinate object that changes every time we print it would be quite useless. We can get the compiler to enforce this observation by adding const after the parameter list for the method; now, the compiler will not let us change myX or myY in the method definition.

Also, we've put print() in the public section of the class. This is quite typical. It's very common for programmers to print out coordinates in their programs, so it's an operation that we want to make available to everyone. Hence, it goes in the public section.

Whenever you declare a method, take a bit of time to think if the method should change the instance variables of the class. If you even suspect they shouldn't change, add the const. You can always take it away later if you discover you do need to change the contents of an instance variable. (We'll see a method that does very soon.)

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

To define print(), we must inform the compiler that this is a method of a class. This is done by preceding the name of the method with the name of the class of which the method is a member and the scope operator (::). That is, we would define print() as a method of our Coordinate class as follows:

inline void Coordinate::print(ostream & out) const
{
  out << '(' << myX << ',' << myY << ')';
}

Note that const must be present in both the prototype and the definition of a method that does not modify its instance variables. In contrast, inline is used only with the definition, not the prototype.

Let's consider what happens when this method is invoked. As with anything class related, this makes the most sense in an internal perspective: "I am a Coordinate object, and when someone sends me a message telling me to print myself out, I send a left parenthesis, my x-coordinate, a comma, my y-coordinate, and a right parenthesis to the provided ostream." Note how the "my" prefix makes the code similar to this statement; also note the message metaphor.

Let's see if you have the hang of this:

Question #11.1: If point is a Coordinate object whose x-coordinate is 3 and whose y-coordinate is 4, then what does the statement point.print(cout); display?

Question #11.2: If origin is a Coordinate object whose x-coordinate is 0 and whose y-coordinate is 0, then what does the statement origin.print(cerr); display?

As seen in the method definition above, a method can directly access the private instance variables of the object. Otherwise, the private instance variables would remain hidden to all code everywhere, making them quite useless. Someone should be able to access them, and why not myself? If I'm a Coordinate object and someone asks me to print myself, I should be allowed to access my private instance variables.

Using all of this, prototype and define a similar print() method for your Fraction class. Write the method so that when oldMeasure is a Fraction whose numerator is 3 and whose denominator is 4, then a message:

oldMeasure.print(cout);
will display
3/4

Prototype this method in the public section of class Fraction, and define it as an inline method following the declaration of class Fraction in Fraction.h. Check the syntax of your method, and continue when it is correct.

Constructors

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

For example, what should happen when we declare a new Coordinate object like so:

Coordinate point;
Presently, there would be junk in this object's instance variables. It seems reasonable, though, to initialize the x- and y-coordinates to 0.

We might specify this as a postcondition (as part of a specification):

Specification:
postcondition: myX == 0.0 and myY == 0.0.

A constructor does not return anything to its caller; it initializes the instance variables of an object when that object is defined. We specify this behavior as a boolean expression which is true when the constructor terminates. Such an expression is called a postcondition, since it is a condition that holds true after (i.e., "post") the constructor finishes.

A postcondition is not code you should write. It's a test that should be true if you tested it at the end of the constructor. You can test a postcondition if you want (using an assert()), and this may be a good idea (especially if there's a lot of code and there's a bug you can't find). But it's not necessary. Instead, a postcondition indicates what other code you should write. In this case, what code can I execute so that myX is 0.0? The answer is below.

In order for a constructor to be a member of a class, its prototype must appear within the class. Unlike other functions, C++ determines the name of this function:

Rule: The name of a constructor is always the name of the class.

So we prototype this constructor in the public section of class Coordinate, as follows:

class Coordinate
{
  public:
    Coordinate();
    void print(ostream & out) const;
  private:
    double myX, myY;
};

Unlike our print() method, a constructor initializes (i.e., modifies) the instance variables of a class. As a result, it should not (and cannot) be prototyped or defined as const. So the "missing" const is understandable.

However, where's the return type?!! Every function has had a return type. C++ insists on it. Well, in fact, C++ will equally insist that a constructor does not have a return type. As observed above, a constructor never returns anything to its caller; it initializes its instance variables. The object itself has already been created, so that doesn't have to be returned. It just needs to be initialized. So constructors do not need a return type.

As with the print() method, we want everyone to be able to construct Coordinate objects, so the prototype should be placed in the public section of the class.

Also, as before, simple definitions should be placed in the class header file, designated as inline functions. To define an inline Coordinate constructor, we thus write this funny-looking definition in the header file:

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

Given this definition, when a Coordinate object is defined, the compiler will call this constructor to initialize the new object, setting the object's myX and myY instance variables to zero.

The pattern for a constructor is thus:

ClassName::ClassName(ParameterList)
{
  StatementList
}
where the first ClassName refers to the name of the class, the second ClassName names the constructor, and StatementList is a sequence of statements that initialize the instance variables of the class.

Constructors can take parameters, which are defined as they would be for any other function, and any valid C++ statement can appear in the body of such a constructor.

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

Specification:
postcondition: myNumerator == 0 and myDenominator == 1.
That is, the definition:
Fraction oldMeasure;
should initialize the instance variables of oldMeasure appropriately to represent the fraction 0/1. (Remember that division by 0 is a Bad Thing, so initializing it to 0/0 is not advisable.)

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

A Second Constructor

A class can have multiple constructors, so long as each definition is distinct in either the number or the type of its parameters. Defining the same function multiple times is called overloading the function. Overloading works for normal functions, methods, and constructors.

Suppose that we would like to be able to explicitly initialize the x- and y-coordinates of a Coordinate object to two values specified by the programmer creating the object. We can specify this as follows:

Specification:
receive: x and y, two double values.
postcondition: myX == x and myY == y.

We can overload the Coordinate constructor with a second definition, one that takes two double arguments and uses them to initialize our instance variables:

inline Coordinate::Coordinate(double x, double y)
{
  myX = x;
  myY = y;
}

Note the benefit of using the "my" prefix: we don't have to come up with silly or awkward names for the parameters here. If we called the instance variables x and y, we'd have to come up with different names for these parameters. This convention also clarifies the internal perspective: myX is my x-coordinate while x is an x-coordinate that something else has handed to me.

As usual for such a simple function, this constructor is defined inline in the header file, and like all methods of a class, its prototype would be placed in the public section of class Coordinate:

class Coordinate
{
  public:
    Coordinate();
    Coordinate(double x, double y);
    void print(ostream & out) const;
  private:
    double myX, myY;
};
We can declare Coordinate objects to invoke this constructor:
Coordinate point1,
           point2(1.2, 3.4);
Our first constructor initializes point1 since its declaration has no arguments and the first constructor has no parameters. Our second constructor initializes point2 since its declaration has two double arguments and the second constructor has two double parameters. After these declarations, we have these objects:

Using this information, define and prototype a second Fraction constructor that satisfies this specification:

Specification:
receive: numerator and denominator, two integers.
precondition: denominator is not 0.
postcondition: myNumerator == numerator and myDenominator == denominator.
Consequently, the definitions
Fraction oldMeasure;
...
Fraction scaleFactor(1, 6);
should initialize oldMeasure to 0/1, and initialize scaleFactor to 1/6.

Use a call to assert() to ensure the precondition.

Use the compiler to test the syntax of what you have written. When the syntax is correct, use pierre1.cpp to test what you have done, by inserting calls to print() to display their values:

...
oldMeasure.print(cout);
...
scaleFactor.print(cout);

Also try initializing scaleFactor with a zero denominator. Make sure that the assert() is triggered.

When your constructors and methods are working correctly, remove this test code from pierre1.cpp.

Accessor Methods

It might be useful to be able to extract the x- and y-coordinates of a Coordinate object. It is common to need the values stored in an object, and the methods that return these values are generally known as accessor methods.

As with most classes, the accessor methods for the Coordinate class have very simple specifications:

Specification:
return: my x-coordinate.
and
Specification:
return: my y-coordinate.

Since these methods do not modify any of the instance variables, we can declare them as const methods. Generally, accessor methods begin with the prefix "get" followed by the name of the attribute (without "my"). So, two new prototypes are added:

class Coordinate
{
  public:
    Coordinate();
    Coordinate(double x, double y);
    double getX() const;
    double getY() const;
    void print(ostream & out) const;
  private:
    double myX, myY;
};

We would then define these simple methods in the header file as follows:

inline double Coordinate::getX() const
{
  return myX;
}

inline double Coordinate::getY() const
{
  return myY;
}

Suppose we declare two Coordinate objects point1 and point2:

Question #11.3: What do these expression evaluate to: point1.getX() and point2.getY()?

This is another reason for the "my" prefix. We can use the my-free names as the name of a method that accesses the value of that instance variable.

Using this information, add to class Fraction an accessor method getNumerator() that satisfies this specification:

Specification:
return: myNumerator.
Also write an accessor method getDenominator() that satisfies this specification:
Specification:
return: myDenominator.
Since these are simple methods, define them inline following the class declaration in the header file. Test their syntax by compiling the code 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 that looked like this:

(3,4)
A user would type this in, or perhaps a program would read this in from a file.

We can specify the problem as follows:

Specification:
receive: in, 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 && myY == y.

We prototype this method in the class (as with all methods), although this one is not const because it modifies the instance variables:

class Coordinate
{
  public:
    Coordinate();
    Coordinate(double x, double y);
    double getX() const;
    double getY() const;
    void read(istream & in);
    void print(ostream & out) const;
  private:
    double myX, myY;
};

We can define read() as a method that satisfies the specification, as follows:

void Coordinate::read(istream & in)
{
  char ch;         // for reading ( , and )
  in >> ch         // read '('
     >> myX        // read x-coordinate
     >> ch         // read ','
     >> myY        // read y-coordinate
     >> ch;        // read ')'
}

Testing pre- and postconditions for an input method is tricky at best, so don't worry that we haven't tested them here. They're good to write down anyway so that we're clear on what we expect.

The input specification indicates that a coordinate in the input will have punctuation around it: parentheses and a comma. The ch variable is used to read in these characters and throw them away. It's useful to have them in the input to make the input more readable, but they're frivolous in a Coordinate object since every Coordinate object is written with this punctuation. We only need it in the input and output.

Given the length of this method, it's pressing the boundaries of what some compilers define as "simple"; some won't allow us to declare it inline. As a result, we define it in the implementation file (without the keyword inline).

With this method, the statements

Coordinate point;
point.read(cin);
reads a Coordinate of the form (x,y) from cin.

Using this information, define and prototype an input method named read() for class Fraction. Your method should satisfy this specification:

Specification:
receive: in, an istream.
precondition: in contains a Fraction value of the form n/d and d is not zero.
input: n/d, from in.
passback: in, with Fraction n/d extracted from it.
postcondition: myNumerator == n and myDenominator == d.

Some differences from the read() of Coordinate:

After you've written and successfully compiled this new method, you should be able to uncomment the statements:

oldMeasure.read(cin);
...
scaleFactor.read(cin);

Test your input method by adding print() statements after the read() statements in pierre1.cpp to test the input routines. Compile and run the program, and continue when read() works correctly. Remove the test print() statements.

Fractional Multiplication

We have seen that methods like constructors can be overloaded. In addition, C++ allows us to overload operators, such as the arithmetic operators (+, -, *, /, and %). However to do so, we need to rethink the way expressions work.

Suppose that we want to permit two Coordinate objects to be added together. In C++'s object-oriented world, an expression like

point1 + 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, and it doesn't particularly warrant testing at the end of the method.

According to our specification, this operation does not modify the instance variables of the Coordinate that receives it, and so we write the following prototype:

class Coordinate
{
  public:
    Coordinate();
    Coordinate(double x, double y);
    double getX() const;
    double getY() const;
    void read(istream & in);
    void print(ostream & out) const;
    Coordinate operator+(const Coordinate & point2) const;
  private:
    double myX, myY;
};

Not only is the method itself const, but the parameter is const as well. We don't change either object. We pass the parameter by reference because it's not a primitive type; recall that passing by reference is a bit faster than by value for non-primitive types.

One way to define this method is as follows:

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

This function illustrates that, for any overloadable operator Δ, we can use the notation operatorΔ as the name of a function that overloads Δ with a new definition.

Once such a method has been prototyped and defined as a member of class Coordinate, we can write familiar looking expressions, such as

point1 + 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 method call:

point1.operator+(point2)

While it is useful to overload all of the arithmetic operators for a Fraction, the particular operation that we need in order to solve our problem is multiplication (the others, we leave for the exercises). From the preceding discussion, it should be evident that we need to overload operator* so that the expression in pierre1.cpp:

oldMeasure * scaleFactor
can be used to multiply the two Fraction objects oldMeasure and scaleFactor.

However, the math here is a bit more involved that in the past examples and tasks. We can get some insight into the problem by working some simple examples:

We multiply the numerators together and the denominators together, and then we simplify.

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 instance variables and then simplifying the resulting Fraction.

For the moment, ignore the problem of simplifying a Fraction. Extend your Fraction class with a definition of operator* that can be used to multiply two Fraction objects. When we add the code to simplify result, this method will be reasonably complicated, so define it in the implementation file (and, of course, prototype it in the header file, in the class definition). Test the correctness of what you have written by uncommenting the lines in pierre1.cpp that that compute and output newMeasure. Continue when your multiplication operation yields correct (if unsimplified) results.

The main deficiency of our implementation of operator* is its inability to simplify improper fractions. That is, our multiplication operation would be improved if class Fraction had a simplify() operation, such that fractions like: 2/6, 6/12, 12/4 could be simplified to 1/3, 1/2, and 3/1, respectively.

Such an operation is useful to keep fractional results as simple and easy to read as possible. To provide this capability, we will implement a Fraction method named simplify(). In a method like operator* which constructs its answer in a Fraction named result, simplify() can be invoked like so:

result.simplify();
to simplify the Fraction in result.

It will help to phrase this in message passing terms. This calls says, "Hey, result! Simplify yourself!" result then goes off and simplifies itself (e.g., changes itself from 12/4 to 3/1). I don't expect anything back; I've told result to do all the work and change itself.

That takes care of result in the multiplication operation, but what about the definition of simplify()? We shift our perspective to the Fraction that's been told to simplify itself. There are a number of ways to simplify a fraction. One way is the following algorithm:

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

The specification for this method is thus:

Specification:
postcondition: myNumerator and myDenominator do not share an common factors (i.e., the fraction is simplified).

Remember that simplify() doesn't need any extra information, except the Fraction object that it's invoked on, so there are no parameters. Also, the object that receives this message changes itself, so it cannot be declared const. And there's no need to return anything. So we end up with only a postcondition in our specification.

The implementation file gcd.cpp contains a function greatestCommonDivisor() that implements Euclid's algorithm for finding the greatest common divisor of two integers. Using greatestCommonDivisor() and the preceding algorithm, define simplify() as a method of class Fraction.

Since this is a complicated operation, define it in the implementation file, and only prototype it in the header file.

We have now provided all of the operations needed by pierre1.cpp, so the complete program should be operable and can be used to test the operations of our Fraction class.

Part II. pierre2.cpp

In this second part of this exercise, we add the functionality to class Fraction in order for pierre2.cpp to work properly, so use your text editor to open it for editing.

Output Revisited

While we have provided the capability to output a Fraction value via a print() method, doing so requires that we write clumsy code like:

cout << "\nThe converted measurement is: ";
newMeasure.print(cout);
cout << "\n\n";
instead of elegant code like:
cout << "\nThe converted measurement is: " << newMeasure << "\n\n";

Our print() method does solve the problem, but its solution doesn't fit in particularly well with the rest of the iostream library operations. It would be preferable if we could use the usual insertion operator (<<) to display a Fraction value.

Let's revisit our Coordinate class. We would like to be able to write this:

cout << point << endl;
And this should display the Coordinate object named point on cout.

Well, << is an operator just like + or *, and overloading those worked well, so we could add a method to class ostream overloading operator<< with a new definition to display a Coordinate value. Then the compiler could treat an expression like

cout << 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 method of Coordinate, but C++ would then require us to invoke the method like so:

point << cout;
Yikes! That looks like cout is being sent to point---the complete opposite of what we want. Technically, we could use the >> operator instead, but we'd still have the output stream on the right, and we're used to our streams on the far left. Everyone keeps their streams on the left.

Instead, we can overload the insertion operator (<<) as a normal function (i.e., not as a method) that takes an ostream (e.g., cout) and a Coordinate (e.g., point) as its operands. That is, an expression

cout << 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 function in the header file, following the class declaration:

inline ostream & operator<<(ostream & out, const Coordinate & coord)
{
  coord.print(out);
  return out;
}

There are several subtle points in this definition that need further explanation:

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

Specification:
receive: out, an ostream, and aFraction, a Fraction.
precondition: aFraction.getNumerator() == n and aFraction.getDenominator() == 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. Uncomment the appropriate line in pierre2.cpp, and test it out. You haven't written the input operator yet, so don't uncomment those lines yet. Just use the default values in the Fractions. Continue when pierre2 compiles and executes correctly (as far as it should for now).

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 (i.e., extractor and insertion) operators:

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

Using this information, overload the extraction operator for class Fraction, so that a user can enter

3/4
to input the fraction 3/4.

Uncomment the remaining statements in pierre2.cpp, and test the correctness of these operations. Your Fraction class should now have sufficient functionality for Chef Pièrre to solve his problem using pierre2.

When everything in your Fraction class is correct, complete Fraction.doc.

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 instance variables. By default, C++ will not allow any other function to access private instance variables.

But suppose we had not written the read() method for class Coordinate, and wanted to overload operator>> in order to input Coordinate values. We'd probably write what we do have for read() in the input operator:

istream & operator>>(istream & in, Coordinate & coord)
{
  char ch;
  in >> ch          // consume (
     >> coord.myX   // read x-coordinate
     >> ch          // consume ,
     >> coord.myY   // read y-coordinate
     >> ch;         // consume )
}
Syntactically, this is 100% correct. However, semantically, the compiler does not allow this and will generate compilation errors for the accesses to coord's instance variables. As a non-method, this operator does not have the privilege of accessing the private instance variables. However, we might like to be able to grant this permission.

For such situations, C++ provides the friend mechanism. If a class names a function as a friend with the keyword friend, then that non-member function is permitted to access the private section of the class. A function is declared as a friend by including a prototype of the function preceded by the keyword friend in the class definition. Thus, we would have to write this in our Coordinate class:

class Coordinate
{
  public:
    Coordinate();
    Coordinate(double x, double y);
    double getX() const;
    double getY() const;
    Coordinate operator+(const Coordinate & point2) const;
    friend istream & operator>>(istream & in, Coordinate & coord);
  private:
    double myX, myY;
};

Placing the friend keyword before a function prototype in a class thus has two effects:

As we have seen in this exercise, an object-oriented programmer can usually find other ways to implement an operation without resorting to the friend mechanism. In the object-oriented world, solving a problem through the use of methods is generally preferred to solving it through use of the friend mechanism. As a result, friend functions tend to be used only when the object-oriented alternatives are too inefficient for a given situation. Nevertheless, they are a part of the C++ language, and you should know the distinction between a method and a friend function of a class.

You could make a similar change to your Fraction class, even just prototype the input and output operators as friend functions, but it's not necessary.

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 method (or a friend function).
  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! If you can imagine it, you can write a class for it.

The class thus provides the foundation for object-oriented programming, and mastering the use of classes is essential for anyone wishing to program in the object-oriented world.

Learning to design and implement classes is an acquired skill, so feel free to practice by creating software models of objects you see in the world around you! You cannot practice this too much.

Submit

Turn in a copy of your code and sample executions of both drivers. Answer the questions posed in this lab exercise in comments in the Fraction.doc documentation file.

Terminology

accessor method, attribute, class, construct (an object), construct, friend mechanism, inline method, instance variable, internal perspective, message, method, object-oriented programming, overloading, postcondition, private, public, reference return-type, scope operator
Lab Home Page | Prelab Questions | Homework Projects
© 2003 by Prentice Hall. All rights reserved.
Report all errors to Jeremy D. Frens.