9.15.2. fraction 1 Example

Time: 00:06:05 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | Slides (PDF)

A fraction comprises two integers, a numerator, and a denominator. Fractions can be added, subtracted, multiplied, and divided. These variables and functions are just enough detail to make fractions interesting demonstrations of classes and objects. The fraction example will consist of three files that follow the same pattern as the class version of the Time example. Together, fraction.h and fraction.cpp form a general server or supplier that any class needing basic fraction operations may use. The calc.cpp file demonstrates a client program that uses the fraction class.

A UML class diagram for the fraction class. The diagram contains the following information:
fraction
----------------------------
-numerator : int
-denominator: int
----------------------------
+fraction(n : int = 0, d : int = 1)
+add(f : fraction) : fraction
+sub(f : fraction) : fraction
+mult(f : fraction) : fraction
+div(f : fraction) : fraction
+print() : void
+read() : void
class fraction
{
    private:
	int	 numerator;
	int	 denominator;

    public:
	fraction(int n = 0, int d = 1);
	fraction add(fraction f2) const;
	fraction sub(fraction f2) const;
	fraction mult(fraction f2) const;
	fraction div(fraction f2) const;
	void	 print() const;
	void	 read();
};
(a)(b)
The fraction class. The fraction class has one constructor (that serves as three), four arithmetic functions (add, sub, mult, and div), and two I/O functions (print and read).
  1. A UML class diagram for the fraction class.
  2. The C++ class specification is contained in fraction.h, which is available at the bottom of the page.
Class diagrams and specifications both show what functions the fraction class has but not how the functions are implemented - that is, they don't show what statements are in the bodies of the functions. Software developers turn to the program's requirements to determine how to implement the functions.

The fraction Class Requirements

We can write even small, simple programs in many ways. As the size and complexity increase, so does the number of ways we can write it. Adding to this complexity is a contemporary practice of building large, complex systems from individual programs - that means that many programs must work together to solve a large, complex problem. So, sometimes when we write a program, we can choose just how we want to implement it. However, software developers often create programs for customers who may have additional requirements. The requirements may seem arbitrary and stifling our creativity - we may even feel micromanaged. But our code may need to integrate with existing code or a larger system. The requirements are necessary to ensure that our new code integrates with the existing code or behaves as the customer expects.

A requirements document is a formal statement of what a program or system of programs must do. It generally does not specify how the program or system accomplishes its tasks. This separation of "what" from "how" is another way of looking at the separation of a class's public interface from its implementation. Large projects or systems may consist of many individual but interconnected programs; in these cases, the requirements may form rather large documents. The requirements for individual programs are generally much smaller, but we must study them carefully to ensure that the program completes all of its tasks accurately. The following requirements are arbitrary, but they give us much-needed practice in reading, understanding, and fulfilling a set of requirements.

  1. The fraction class shall have a single public constructor that serves as three distinct constructors:
    1. A default constructor that makes an "empty" or "zero" fraction object. A default constructor call for the fraction class looks like this: fraction f;
    2. A conversion constructor that converts an integer into a fraction. For example, the integer 5 is equivalent to the fraction 5/1. The conversion constructor will have one parameter that will initialize the fraction's numerator; the constructor will always set the denominator to 1. A fraction object is instantiated with this constructor as: fraction f(5);
    3. A general constructor that initializes the fraction's numerator and denominator. Creating an object with this constructor looks like this: fraction f(2, 3);, which represents the fraction 2/3
  2. None of the arithmetic operations shall change either fraction object
  3. Each arithmetic operation shall create and return a new fraction object to represent the result of the operation
  4. Each fraction object returned by an arithmetic operation shall be in lowest terms (1/2 rather than 2/4, improper fractions like 5/3 are okay)
  5. For efficiency and to avoid duplicating code, reducing the fraction to the lowest terms shall be done by the constructor and not by the arithmetic functions
  6. The output function shall display a fraction with the numerator and the denominator separated by a forward slash: 2/3 or 5/3
  7. The input function shall prompt for and read the numerator, then prompt for and read the denominator

The Constructor

Following best practices for class implementation, the constructor will use an initializer list to initialize the numerator and the denominator. We can make one constructor serve all three roles (requirement 1) by using default arguments. We can have the constructor reduce the returned fraction to the lowest terms (requirement 5) by finding the greatest common divisor and dividing the numerator and denominator by that value.

fraction.hfraction.cpp
fraction(int n = 0, int d = 1);




 
fraction::fraction(int n, int d) : numerator(n), denominator(d)
{
	int	common = gcd(numerator, denominator);
	numerator /= common;
	denominator /= common;
}
The fraction constructor. The function prototype, including the default arguments, is part of the class specification located in the fraction header file. We place the function definition in the fraction source code file. The initializer list initializes the member variables numerator and denominator before the function body executes; so, these variables are ready for use when the constructor calls the gcd function. The gcd function (included with the downloadable code at the bottom of the page) calculates the greatest common divisor of its arguments - the largest number that evenly divides both arguments. For example, gcd(10,15) is 5 and 10/15 = 2/3.

Designing The Arithmetic Functions

The four arithmetic operations are long-established and well-defined for fractions, so you undoubtedly learned how to do basic fraction arithmetic long ago. The next figure presents the formulas for fraction arithmetic using a familiar notation that is quite suitable as a beginning point for programming the fraction class.

\[ \begin{align} Addition: & \quad {a \over b} + {c \over d} = {ad + bc \over bd} \\ \\ Subtraction: & \quad {a \over b} - {c \over d} = {ad - bc \over bd} \\ \\ Multiplication: & \quad {a \over b} \times {c \over d} = {ac \over bd} \\ \\ Division: & \quad {a \over b} \div {c \over d} = {a \over b} \times {d \over c} = {ad \over bc} \end{align} \]
The fraction arithmetic formulas. Given two fractions, a/b and c/d, the formulas show how to calculate the four basic arithmetic operations. See Basic Math Formulas. A fraction object stores the fraction's numerator and denominator as two separate integers. So, within a single fraction, the horizontal lines do NOT mean to divide; the lines separate the numerator from the denominator.

The formulas describe how to calculate the arithmetic operations. Our main task at this point is translating those formulas into C++ code. We translated much more complex formulas in previous chapters, but this time we must also translate them into object-oriented C++. So, our next step is to form a conceptual translation - that is, not to the final C++ functions but to an intermediate form intended to help bridge the gap between the formulas and the code.

f1 = a/b
f2 = c/d
f3 = f1 + f2 = (a*d + b*c) / (b*d)
(a)
f3.numerator = f1.numerator * f2.denominator + f1.denominator * f2.numerator;
f3.denominator = f1.denominator * f2.denominator;
(b)
Converting the fraction formulas into C++ code.
  1. In the formulas, a, b, c, and d represent the numerators and denominators of two fraction operands. a and b represent the numerator and denominator of the left hand fraction, and c and d represent the numerator and denominator of the right hand fraction. When translated into C++ functions, each arithmetic function must create a new instance of the fraction class to store the result of the operation. So, the part of the formulas on the right side of the equals sign (not quite the same as the C++ assignment operator) represents the steps or operations needed to calculate the numerator and the denominator of the new fraction object.
  2. The C++ code is written in a way to help ease the transition between the fraction formulas and the fraction class member functions, so it's not quite in the final form.

Implementing The Arithmetic Functions

Depending on its complexity, there are generally many ways to write a function. The fraction add and mult functions are used to illustrate two slightly different ways. All four arithmetic functions are contained in fraction.cpp at the bottom of the page.

fraction fraction::add(fraction f2) const
{
	int	n = numerator * f2.denominator + f2.numerator * denominator;
	int	d = denominator * f2.denominator;

	return fraction(n, d);
}
fraction fraction::mult(fraction f2) const
{
	return fraction(numerator * f2.numerator, denominator * f2.denominator);
}
Implementing the fraction arithmetic functions. One of the most striking aspects, which is often confusing for new object-oriented programmers, is that the arithmetic functions only have one explicit parameter in the parentheses. How can we add or multiply two fractions when we only pass one to the function? Recall that these are member functions that are bound to an implicit argument. So, the functions operate on two fractions: one implicit and one explicit. The final versions of the add and mult functions (sub and div are similar) differ from Figure 3(b) in two very significant ways:
  1. The final versions are implemented as member functions, so the full function name includes fraction:: at the beginning. And the program accesses the member variables of the left-hand fraction object without using a variable name (such as f1 in Figure 3).
  2. Although not illustrated in Figure 3(b), the code assumes that f3 was instantiated prior to the illustrated statements (likely using a default constructor). This sequence will produce a numerically valid result, but it has one flaw. The arithmetic functions must return a new fraction object that is reduced to the lowest terms. However, the reduction to the lowest terms takes place in the constructor. The constructor cannot reduce the fraction if the program instantiates the new fraction object before calculating the numerator and denominator. After calculating the numerator and denominator, the final versions instantiate the new fraction, which allows the constructor to reduce the fraction to the lowest terms, satisfying requirement 4. Returning a new fraction object to represent the result of the operation satisfies requirement 3.
The explicit fraction argument f2 is passed by value and is therefore unchangeable. The addition of the const keyword makes the implicit fraction object, the object bound to the functions through the this pointer, unchangeable as well. Making both objects unchangeable satisfies requirement 2 above.

The two remaining fraction functions are small and relatively simple.

void fraction::print() const
{
	cout << endl << numerator << "/" << denominator << endl;
}
void fraction::read()
{
	cout << "Please enter the numerator: ";
	cin >> numerator;
	cout << "Please enter the denominator: ";
	cin >> denominator;
}
The fraction input and output (I/O) functions. It is often up to a programmer to determine the best way to display or print an object. However, requirement 6 specifies a fraction's format when the program outputs or displays it - compare this code to the requirement. The print function does need to (and shouldn't) change the fraction object when printing it, and the "const" keyword prevents the function from making any changes.

Similarly, requirement 7 specifies the behavior of the input or read function, which must change the object, so the const keyword is not used here. Again, compare the requirement to the C++ code. There wasn't a requirement for the read function to reduce the fraction to the lowest terms or check for a zero denominator. However, it's fairly straightforward to do both. Adding a private helper function, and moving the reduction code from the constructor to it, makes it easy to reduce the fraction to the lowest terms in both the constructor and the read function. The read function throws an exception if the user enters a zero denominator. A second version of the fraction program demonstrates these variations. Please see fraction2 in the downloadable code at the bottom of the page.

void fraction::read()
{
	cin >> numerator;
	cin >> denominator;

	if (denominator == 0)
		throw "denominator = 0 error";

	reduce();
}
fraction f;

try
{
	f.read();
}
catch(char* error)
{
	cerr << error << endl;
}
Throwing and catching an exception. Although there was no requirement to check the denominator to ensure that it is non-zero, it is an error for a simple fraction to have a zero denominator. We can include a test for this error, and the program can throw an exception if it is detected. C++ is quite flexible, and programmers can use any data type as an exception. In this example, the program throws a C-string. Version 2 of the program also demonstrates this option.
  1. Throwing an exception
  2. Catching an exception

Fraction Calculator: A Fraction Client

Together, fraction.h and fraction.cpp form a supplier. To make a complete program, we need a client - code that uses the fraction class. We begin with calc.cpp program from chapter 3. We need to make four modifications to the calc program to make it into a simple fraction calculator:

  1. #include <fraction.h>
  2. Replace double with fraction
  3. Replace cin >> with read and replace cout << with print (the second replacement only applies to printing the numeric results and not to the prompts)
  4. Replace the arithmetic operators +, -, *, and / with the corresponding fraction functions add, sub, mult, and div
switch (choice)
{
	case 'A':
	case 'a':
		cout << "Adding" << endl;
		cout << "enter the first operand: ";
		left.read();
		cout << "enter the second operand: ";
		right.read();
		result = left.add(right);
		result.print();
		break;
			.
			.
			.
Using the fraction class. An excerpt from calc.cpp demonstrates how a switch statement carries out the arithmetic operation selected by the end user. It also demonstrates the syntax for calling the read, print, and add functions (the code for sub, mult, and div is very similar).

Downloadable Code

Tab stops are set at 8 spaces - the default for the gvim editor I used to write the code.

ViewDownload
fraction.h fraction.h
fraction.cpp fraction.cpp
calc.cpp calc.cpp
fraction2.h fraction2.h
fraction2.cpp fraction2.cpp
calc2.cpp calc2.cpp