11.8.2. fraction 2 Example (Overloaded Operators Version)

Time: 00:4:21 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | Slides (PDF)
Review

Previous sections presented abridged versions of the fraction class and many of its functions as examples of overloaded operators, implemented as either member or friend functions. This section presents the complete and elaborated example, organizing it into multiple figures to simplify the presentation.

#pragma once

#include <iostream>
using namespace std;

class fraction
{
    private:
        int     numerator;						// (a)
        int     denominator;

    public:
                  fraction(int n = 0, int d = 1);			// (b)

        friend    fraction operator+(fraction f1, fraction f2);		// (c)
        friend    fraction operator-(fraction f1, fraction f2);
        friend    fraction operator*(fraction f1, fraction f2);
        friend    fraction operator/(fraction f1, fraction f2);

        friend    ostream& operator<<(ostream& out, fraction& f);	// (d)
        friend    istream& operator>>(istream& in, fraction& f);
 
   private:
        void      reduce();						// (e)
        int       gcd(int, int);
};
fraction.h: fraction 2 example. In this example, the fraction class specification only contains function prototypes, with the function definitions or bodies in a source code file. When programmers separate a class specification and the function definitions, the friend keyword only appears in the specification.
  1. A fraction has two member variables or fields: numerator and denominator.
  2. The constructor uses default arguments and an initializer list. The default arguments are part of the prototype, but the initializer list is part of the function definition.
  3. The fraction class supports four arithmetic operations, each implemented as an overloaded operator.
  4. The fraction class overloads the inserter and extractor operators.
  5. The fraction class has two helper functions it uses internally but does not provide as services to clients - the functions are not included in the class's public interface.
#include <iostream>
#include "fraction.h"
using namespace std;

fraction::fraction(int n, int d) : numerator(n), denominator(d)
{
    reduce();
}
fraction.cpp: constructor. Although this constructor version is small enough to include in the class specification, the example defines it in the source code file to demonstrate the default argument and initializer list syntax necessary when separating function prototypes and definitions. The default arguments allow client programs to call the constructor in three ways: The initializer list initializes numerator and denominator before the function body runs, ensuring that when reduce runs, it operates on valid data, reducing the fraction to lowest terms.
fraction operator+(fraction f1, fraction f2)
{
    int    n = f1.numerator * f2.denominator +
                f2.numerator * f1.denominator;
    int    d = f1.denominator * f2.denominator;

    return fraction(n, d);
}
fraction operator-(fraction f1, fraction f2)
{
    int    n = f1.numerator * f2.denominator -
                f2.numerator * f1.denominator;
    int    d = f1.denominator * f2.denominator;

    return fraction(n, d);
}
fraction operator*(fraction f1, fraction f2)
{
    int    n = f1.numerator * f2.numerator;
    int    d = f1.denominator * f2.denominator;

    return fraction(n, d);
}
fraction operator/(fraction f1, fraction f2)
{
    int    n = f1.numerator * f2.denominator;
    int    d = f1.denominator * f2.numerator;

    return fraction(n, d);
}
fraction.cpp: fraction arithmetic operators. The class implements the arithmetic operators as friend functions - not members. So, the program must explicitly pass all arguments in parentheses and use the parameter names when accessing the objects' member variables. The advantage of constructing the new fraction at the end of the function, after it calculates the numerator and denominator, is that it allows the constructor to reduce the fraction to the lowest terms.
ostream& operator<<(ostream& out, fraction& f)
{
    cout << endl << f.numerator << "/" << f.denominator << endl;

    return out;
}
 
 
istream& operator>>(istream& in, fraction& f)
{
    cin >> f.numerator;
    cin >> f.denominator;
    f.reduce();

    return in;
}
(a)(b)
fraction.cpp: fraction I/O operators. The overloaded I/O operators follow a rigid pattern, but each version is overloaded for a different class, implying that their bodies contain unique instructions. The functions' instructions or statements depend on the target class' member variables. These functions target the fraction class, accessing its numerator and denominator.
  1. Programmers choose how a printed fraction object should look, and format it accordingly. The out parameter may refer to any output stream - the console, a file, etc.
  2. The input operator may receive data from the console, a file, a network, etc. Removing the prompts generalizes the function, allowing it to read data from any of these sources efficiently. The stream object in may be bound to any input stream drawing data from these sources.
int gcd(int u, int v)
{
    u = (u < 0) ? -u : u;
    v = (v < 0) ? -v : v;

    while (u > 0)
    {
        if (u < v)
        {
            int t = u;    // swap u and v
            u = v;
            v = t;
        }

        u -= v;
    }

    return v;            // the GCD of u and v
}
void fraction::reduce()
{
    int    common = gcd(numerator, denominator);
    numerator /= common;
    denominator /= common;
}
(a)(b)
fraction.cpp: helper functions. All functions provide code reusability by grouping and naming operations that programs use by calling them. Furthermore, they allow programmers to decompose large problems into smaller ones. Programmers implement small solutions as functions and incorporate them into larger ones through function calls. Helper functions work similarly but limit their actions to objects instantiated from a single class.
  1. The gcd function implements Euclid's algorithm for finding the greatest common divisor. The fraction class uses the function to reduce a fraction to its lowest terms. However, the function is only used internally and is not a service typically associated with fractions. So, programmers exclude it from the class's public interface by making it private. See Recursion V. Iteration.
  2. The fraction class automatically reduces a fraction object when it creates or inputs it. By putting the statements in a helper function, the class avoids duplicating them in the constructors and operator>>. The function is appropriately private because all fraction operations reduce the objects when creating or changing them, eliminating the need for applications to perform that operation. If it becomes possible for applications to create unreduced fractions, programmers can make the function public.
#include "fraction.h"
#include <iostream>
using namespace std;

void input(fraction& l, fraction& r);					// (a)

int main()
{
    char        choice;							// (b)

    do									// (c)
    {
        ... code moved to the following figure ...

    } while (choice != 'e' && choice != 'E');				// (d)

    return 0;
}

void input(fraction& l, fraction& r)					// (e)
{
    cout << "Left-hand fraction: numerator <space> denominator: ";
    cin >> l;
    cout << "Right-hand fraction: numerator <space> denominator: ";
    cin >> r;
}
calc.cpp: fraction 2 version. The calc program implements a simple fraction calculator and is the "client" using the fraction class. The file is too long to display in a single figure, so the example moves the while-loop body to the next figure.
  1. Prototype for the input function (e). See Pass by reference with simple data.
  2. The client program, calc, prints a menu of possible operations and saves the user's choice in choice. The program defines choice outside the loop so it is in scope for the test (e).
  3. A do-while loop does most of the work, looping until the user chooses to exit the program.
  4. The loop continues while the choice is neither an 'e' nor 'E.'
  5. The client application "knows" that the console is the source of the fraction data and, therefore, that prompts are appropriate. The input function groups the repetitive operations necessary to read the data. The highlighted statements call the fraction extraction operator, passing cin its first parameter.
    do
    {
        cout << "A\tAdd\n";						// (a)
        cout << "S\tSub\n";
        cout << "M\tMult\n";
        cout << "D\tDiv\n";
        cout << "E\tExit\n";

        cout << "\nChoice?: ";

        cin >> choice;							// (b)
        cin.ignore();

        if (choice == 'E' || choice == 'e')				// (c)
            break;

        fraction    left;						// (d)
        fraction    right;
        fraction    result;

        input(left, right);						// (e)

        switch (choice)							// (f)
        {
            case 'A':
            case 'a':
                result = left + right;
                break;
            case 'S':
            case 's':
                result = left - right;
                break;
            case 'M':
            case 'm':
                result = left * right;
                break;
            case 'D':
            case 'd':
                result = left / right;
                break;
            default:
                cerr << "Unrecognized choice: " << choice << endl;
                break;
        }

        cout << result << endl;

    } while (choice != 'e' && choice != 'E');
calc.cpp: fraction 2, do-while loop. The do-while loop is at the center of the client. The highlighted statements demonstrate the overloaded fraction operators.
  1. Prints a simple menu.
  2. Reads the user's chosen operation.
  3. End the loop when the user selects 'e' or 'E' to exit the program.
  4. Creates fraction objects for the left and right operands and the operation's result.
  5. Reads values into the left and right operands.
  6. Displays the operation's results.

Downloadable Code

ViewDownload
fraction.h fraction.h
fraction.cpp fraction.cpp
calc.cpp calc.cpp