Courses/CS 2124/Lab Manual/Operator Overloading

From A-State Computer Science Wiki
Jump to: navigation, search

Operator Overloading


This lab examines how operators are overloaded in C++. Both operator methods and friend functions will be defined.

Createproject.pngCreate a project oop05 with the empty h and cpp files listed below.

The Fraction Object

A fraction is the ratio, or division, of two numbers, the fraction’s numerator and its denominator. Put the following class definition for a Fraction object in file Fraction.h.

class Fraction {
    Fraction (int n, int d);
    int numerator   = 0;
    int denominator = 1;

The constructor for the class should initialize numerator and denominator from the n and d formal parameters, respectively. If the value of d is zero, we cannot allow the construction to take place. In this case, we have no choice but to throw an exception. Throw an object of type std::overflow_error constructed with the message “Zero denominator is not allowed.” if the value of d was zero. The std::overflow_error type comes from the <stdexcept> header.

Write this constructor now in file Fraction.cpp.

Defining a write() Method

In order to be able to see what the data contents of a Fraction object look like, a method is needed which will display the private data. Add the prototype for method write() to the Fraction class definition.

void write (std::ostream& strm=std::cout) const;

This method has an optional parameter of type std::ostream& that allows the user to specify a custom stream to write the fraction into; if no stream is passed, cout will be the default. (Don’t forget to include <iostream> in your header file.)

As it has no reason to change its object in any way, the method is qualified with const; this causes the compiler to complain if a change is inadvertently attempted.

Write the implementation of the method so that it displays the numerator followed immediately by a forward slash followed immediately by the denominator. The fraction with numerator of one and denominator of two would then display as 1/2. Write function main() in file main.cpp to test this method with the following.

Fraction f1 {3, 5};
cout << "f1: ";
cout << endl;

Defining Addition as a Method

Addition is a practical method to define for Fraction objects. Consider first the addition of an integer to a fraction, such as {\textstyle \frac{1}{2} + 3}. Add another prototype for method add().

Fraction add (int i) const;

Note that the method will return a newly created Fraction object, much in the same sense that the expression {\textstyle 3 + 5} returns the new integer 8. To do this, the method can use a return statement such as the following, where n and d are integers derived from the attributes of the current object and the integer operand of method add().

return Fraction{n, d};

Recall that the assignment operator= is implicitly defined for every class definition to perform member-wise assignment; in the case of class Fraction, the source numerator and denominator are copied to the destination numerator and denominator, respectively. Add the following to function main() to test method add() and the assignment operator.

Fraction f2 = f1.add(2);
cout << "f2: ";
cout << endl;

Overloading the + Operator

Operator overloading allows the first line of the preceding sequence to be replaced with f2 = f1 + 2, giving it a more visually familiar appearance.

C++ allows the keyword operator to be followed by many of the predefined operators as the name of a method or function, e.g. operator+. (The operator must already be defined by C++, and there are a handful of predefined operators which cannot be overloaded; see the textbook.) Add the following prototype to the class Fraction.

Fraction operator+ (int i) const;

Copy the body of method add(); replace add with operator+. Test this new method with the following addition to function main().

Fraction f3 = f1.operator+(2);
cout << "f3: ";
cout << endl;

Of course, this is really little better than using method add(). The syntactical equivalent below is more pleasing to the eye; test it by adding the following to function main().

Fraction f4 = f1 + 2;
cout << "f4: ";
cout << endl;

Overloaded Operators with Object Operands

The + operator can be further overloaded in order to implement the addition of two Fraction objects. Add the following prototype to the Fraction class defintion.

Fraction operator+ (const Fraction& op2) const;

The Fraction parameter op2 is declared to be const since the method will make no changes to it; it can then safely be passed by reference to prevent a copy of the original parameter from being created. Note that two Fraction objects f1 and f2 can be added as either f1 + f2 or f2 + f1. The left-hand operand of an overloaded operator is always the object of the method; the right-hand operand is always represented by the single parameter. Method operator+ will access the private data of its object (the left-hand operand) without qualification; object parameter op2 (the right-hand operand) data is accessed with op2 and the dot operator.

Arithmetically, the addition of fractions is said to be commutative; that is, fractions may be added in either order to get the same result.

This problem will be addressed shortly.

Labcheckpoint.png FOR IN-LAB CREDIT: Explain why the addition method defined here is commutative but the earlier addition method for a fraction and an integer is not.

Add the following to function main() to test the addition operator for two Fraction objects.

cout << "f1 + f2: ";
Fraction f5 = f1 + f2;
cout << endl;
cout << "f2 + f1: ";
Fraction f6 = f2 + f1;
cout << endl;

Operator Associativity and Cascading

Consider an expression involving three operands and two operators, both of which are the same; the associativity of the operator used dictates whether the left or the right instance of the operator is applied to its operands first. (This also applies to two different operators of the same precedence.) The associativity of an overloaded operator is always the same as the operator’s original definition in C++. Since the addition operator returns a Fraction and associates left to right, it can be used in a cascading fashion to add a series of Fraction objects. The cascading use of the addition operator can be tested with the following addition to function main().

cout << "f1 + f2 + f3: ";
Fraction f7 = f1 + f2 + f3;
cout << endl;

Right-Hand Object Operands in friend Functions

As mentioned earlier, in order for operator+ to be commutative, addition of expressions such as {\textstyle 1 + \frac{1}{2}}, where an integer appears on the left of the operator, must be supported. C++ syntax is such that a method cannot be used to do this: the object of the method is always the left-hand operand. One way to allow the object to be the right-hand operand is to use a friend function. friend functions have access to the private data of the class and so are generally not desirable, but this is one situation in which the use of a friend function is marginally acceptable. (Compare this situation to the use of break in a switch statement; the keyword is not considered structured, but it is commonly used in the specific situation given.)

Add the following friend function prototype to the class definition just above the closing curly brace of the class definition.

friend Fraction operator+ (int i, const Fraction& f);

Implement the function; notice the function name is not prefixed with Fraction:: as it is not a method of the class. But, the function will be able to access the private data of the object operand f using the dot operator (that’s what friends are for…).

Extend function main() with the following to test the friend function.

   cout << " 1 + f1:      ";
   Fraction f8 = 1 + f1;
   cout << endl;

A more acceptable way (from the standpoint of least privilege) to accomplish this same result will be examined in the homework.

Reducing Fractions

A fraction such as {\textstyle \frac{2}{4}} is not expressed in the lowest possible terms. Considering how the add function has been implemented, it is inevitable that it will produce non-reduced fractions. Such a fraction can be “reduced” to {\textstyle \frac{1}{2}} by dividing the numerator and denominator by their greatest common divisor (GCD). The GCD of two numbers can be determined by using one of the oldest recorded “programs.” Euclid’s algorithm for finding the greatest common divisor is over 2300 years old.

Create a new file gcd.h and add the following prototype:

int gcd(int a, int b);

The following is an implementation of Euclid’s algorithm for finding the greatest common divisor. You should research this program after the lab so that you can learn how it works. Copy this code into a file named gcd.cpp. It will be implemented as a stand-alone function because it is not directly related to the Fraction class.

#include "gcd.h"

int gcd(int a, int b) {
    int remainder;

    while (b != 0) {
        remainder = a % b;
        a = b;
        b = remainder;

    return a;

Next, add the following prototype to the private section of your Fraction class.

void reduce();

Add an implementation of this function that uses gcd() to determine the greatest common divisor and then divides and assigns (/=) the numerator and denominator by the GCD. Then add calls to reduce() after every place in Fraction that could produce a non-reduced fraction. Although it is safe to add too many calls to reduce(), you should take care not to do so because in other situations this could be a performance problem.

Labcheckpoint.png FOR IN-LAB CREDIT: Demonstrate the reduce function to the lab instructor.

The Stream Insertion Operator <<

Consider again method write(). It allows us to write a fraction into any output stream (even to a file) by passing the stream as the actual parameter as shown below.

cout << "f1: ";
cout << endl;

It would be even more programmer-friendly if we could replace the three lines above with the expression cout << f1 << endl, as would be used for typical predefined data types in C++. Since the Fraction operand does not appear to the left of operator <<, a friend function can be used for the implementation. Add the following prototype to the class definition just before the closing curly brace.

friend std::ostream& operator<< (std::ostream& outfile, const Fraction& f);

Write the implementation of this function. A more acceptable way to implement operator<<() will be examined in the homework.

Note the use of reference parameters in the operator<<() function. As described earlier, this is done to avoid the creation of a new object corresponding to the actual parameter. Such a new object would be created as an exact duplicate of the actual parameter; when the function ends, the destructor is called for this object, possibly crippling the actual parameter at the same time.

The Fraction operand can (and should) be declared const since the function will make no changes to it.

Note also the return type ostream& for the operator<<() function. This is done to allow for cascaded use of <<, such as in cout << f1 << ' ' << f2. The associativity of operators in C++ indicates that << associates left to right. (Remember that neither the precedence nor associativity of an operator can be changed when redefining it.) The expression cout << f1 thus evaluates first. By returning the output file as the result of this sub-expression, the evaluation of the original expression can continue as cout << ' ' << f2, and so on left to right.

Test operator<<() with the following extension to function main().

cout << "f1 (w/ <<):   " << f1 << endl;

It is worth noting that C++ defines operators << and >> as bitwise shift operators. They have been overloaded in the iostream library to work with left-hand stream operands and right-hand operands of the predefined types in C++.

Labcheckpoint.png FOR IN-LAB CREDIT: Demonstrate your code at this point to a lab proctor.

Labsubmitsinglefile.png FOR IN-LAB CREDIT: Zip up these files: All files needed for this laboratory.
Name the file and upload to CSCADE.