The Rational Numbers and Operator Overloading

Introduction

Our rational number system should have all of the arithmetic operation of a number system as well as the comparison operations. Add to this set of operators functions to read, write, initialize, and the assignment operator.

Operators as Functions

Binary operators that are created as class members are actually functions with the first entry the object calling the function. Thus if we were to define the binary operator + for the rational class, it would be declared as follows:

class Rational
{
public:
...
   Rational operator + (const Rational &r);
...
};

This function (operator) is called whenever the compiler encounters an expression x + y in which x is Rational. This works well in the case we have two rationals or in the case x is rational and y is an integer. The first case we clearly have the correct types for a call to the operator. In the second case, the compiler looks to see if Rational has the + operator, then trys to find a way to convert the y into a Rational. It does this by constructing a rational from y using the constructor Rational(x,y=0).

This does not solve the case when x is an integer and y is rational. We know that + is communative, so this should just be a variation on the last case. The problem here is that the + operator is only defined as a function in Rational, and as such is only defined with Rational as the first arguement. To avoid this we define the binary operator +, and all of the other binary operators including the comparison operators as independent operators outside of the class definition.

One way to do this is to make these operators friends of the class. I (and most oop programmers) prefer to just define a sufficient number of functions to implement the binary operators without giving access to the class Rational's data members.

There are arithmetic operators +=, -=, *=, and \= that must be defined as class members, since the left operand must be Rational. These functions can be used to define the standard arithmetic operators. The comparison operators do not correspond to any similar operations, so we will define functions less, lessEqual, greater, greaterEqual, equal and notEqual.

Adding the operators outside of the class allows the compiler to search the entire list of operators until it finds a way of coercing the data to fit one of the operators defined. In this case if x is and integer, and y is a Rational, the compiler knows that integers can be converted to rationals and there is an operator + defined between two rationals. One problem with this scenario is that there may be more than one + operator that has operands that can be coerced. An example of this problem is discussed below.

Class Declaration

Notice we have one private function, simpleRatio. This function is used to insure that all of our rational numbers are stored in the same format.

class Rational
{
public:
	Rational(int = 0, int = 1);
	Rational& operator = (const Rational& r);
	Rational operator - ()const;//unary minus
	Rational operator +=(const Rational &r);
	Rational operator -=(const Rational &r);
	Rational operator /=(const Rational &r);
	Rational operator *=(const Rational &r);
	int less (const Rational & )const;
	int equal (const Rational & )const;
	int lessEqual (const Rational & )const;
	int greater (const Rational & )const;
	int greaterEqual (const Rational &)const;
	int notEqual (const Rational & )const;
	istream& read(istream &s);
	ostream& print(ostream &s)const;
private:
	void simpleRatio();
	int numerator;
	int denominator;
};

We add the binary operator declarations after the class declaration. We also add the stream operators at this point.

ostream& operator << (ostream &s, const Rational &r);
istream& operator >> (istream &s, Rational& r);

Rational operator + (const Rational & r, const Rational & s);
Rational operator - (const Rational & r, const Rational & s);
Rational operator * (const Rational & r, const Rational & s);
Rational operator / (const Rational & r, const Rational & s);
int operator < (const Rational & r, const Rational & s);
int operator == (const Rational & r, const Rational & s);
int operator <= (const Rational & r, const Rational & s);
int operator > (const Rational & r, const Rational & s);
int operator >= (const Rational & r, const Rational & s);
int operator != (const Rational & r, const Rational & s);
To see the complete header click here.

Implementation of the Rational Class

I will implement a few of the functions for Rational and some of the operators associated with Rational. You may see the rest of the file Rational.cpp by clicking here.

Constructor

The first function we will consider is the constructor for the class. This constructor should create a rational number from a numerator and denominator supplied by the user. There are many ways to write the same rational number (1/2, 2/4, 3/6, ....). To avoid confusion and to simplify computation, we will always store rational numbers in reduced form (numerator and denominator relatively prime) with the denominator always positive. The function simpleRatio accomplishes this in the constructor for Rational:

Rational::Rational(int n, int d)
{
	if(d==0) throw "Denominator of a fraction can not be 0" ;
	if (d < 0)
	{
		d = -d;
		n = -n;
	}
	numerator = n;
	denominator = d;
	simpleRatio();
}

Read and Write Functions

We want the stream extractor >> to read rationals in the form 1/2, so the function that reads a rational will do more than read a numerator and denominator and call a constructor. This will also require that the insert stream operator << write rationals in the same format. The extractor is fairly simple since it is defined as a free operator so must call on one of the class functions from Rational.

istream& operator >>(istream &s, Rational &r)
{
	return r.read(s);
}

This operator calls on the Rational class's read function. Read functions take on much the same responsibilities as constructors have to initialize a class object. In addition they also take on the responsibility of destructors, releasing any old memory and allocation appropriate new memory. In this case the numerator and denominator are set back to zero in case the function is asked to read an integer.

In order to maintain the ability to check values to see if they are all numeric, the line is read in as a string. Here I used the primitive c type string. As is always the case when using a local string, I have used a static array rather than a pointer. This avoids many memory management problems. Recall that c strings end in the NULL character. I used this in the final loop which terminates when NULL is encountered.

istream& Rational::read(istream &s)
{
	numerator =0; denominator = 1; // reset the numerator and denominator
	int mult = 1; // mult is used in case the number is negative, 1 is positive.
	char line[80]; // line is the input buffer
	int i=0; // place marker on the line-start at the beginning
	s>>line; // read the line
	if (line[0]== '-') // check for sign at the beginning
	{
		i++; // skip the sign
		mult *= -1; // record if negative- -1 indicates negative
	}

	if (line[0] == '+') // skip positive sign if it exists
		i++;

	while ((line[i])&&(line[i] != '/')) // look for denominator until end of line or /
	{
		if (line[i]>= '0' && line[i] <='9') // must have integers
		{
			numerator = 10*numerator + (line[i] - '0'); //computer value
			i++;
		}
		else // Non integer encountered-error
			throw "You must type a number" ;
	}
	if (line[i] == '/') // if there is a / clear it and look for denominator
	{
		i++;
		denominator = 0;
		if (line[i] == '-') // once again look for the sign
		{
			mult *= -1;
			i++;
		}
	}
	while (line[i]) // The rest of the string is the denominator
	{
		if (line[i]>= '0' && line[i] <='9') // numerical data only
		{
			denominator = 10*denominator + (line[i] - '0');
			i++;
		}
		else
			throw "You must type a number";
	}
	if(denominator==0)// Make sure there was a denominator
	{
		throw "Denominator of a fraction can not be 0";
	  //	denominator = 1;
	}
	else
	  simpleRatio(); // if it's ok, simplify
	numerator *= mult; // Include the sign
	return s; // We're done ship it back.
}

Aritimetic Operators

We'll look at just the addition operators here, but you can look at the other operators by clicking here. The += operator uses the mathematical defintion for the addition of fractions and uses simpleRatio to complete its work.

Rational Rational::operator+=(const Rational &r)
{
	numerator = r.numerator * denominator + r.denominator * numerator;
	denominator = r.denominator*denominator;
	simpleRatio();
	return *this;
}

The main trick with each of the arithmetic operators is to create the return value which is a rational and initialize it with the first parameter of the operator. Then you may add the second to this result using +=.

Rational operator + (const Rational &r, const Rational &s) { Rational result = r; result+=s; return result; }

Operators That Could be Added

Notice that if we try to add (or perform any of the binary operations on an int and a rational, the program converts the int to a rational and uses the rational operator. It is possible to create an operator that converts rationals to doubles, but this will lead to many ambiguous function compiler errors. The best way to get around this problem is to add operators that would perform operations on one rational and one double. To do this you need to add one function to the class Rational that returns it as a double.

double Rational::asDouble()const
{	
	return double(numerator)/denominator;
}

This is a const function since it does not change the value of the object. Do not call this function double-for obvious reasons. A sample of independent + operator follows.

double operator + (const double &d, const Rational &r)
{
	return d + r.asDouble();
}

Complete Example

For a complete example of the use of the class Rational click here.