Assignment: A Rational Class


Assignment 2
Home ]Up ]Assignment 1 ][ Assignment 2 ]Assignment 3 ]Assignment 4 ]Assignment 5 ]Assignment 6 ]Assignment 7 ]Old Assignments ]

 






A Rational Class


Note:
This assignment is intended to give you some
exposure to writing a relatively simple C++
class. In particular, it is intended to show you
how much the C++ compiler is doing for you
automatically — sometimes it does what you want,
and sometimes not.


Rational Numbers


Rational numbers are those numbers that can be represented as
ratios of integers:


1/2	57/234	-2/3	23/405


(Note that all rational numbers are real numbers, but not all
real numbers are, or can be, rational numbers.)


The Rational Class


Implement a class Rational, which supports rational
numbers. The class should support the following:


  • Two int data members, numerator and denominator, which
    are private to the class. These values will be kept in normalized
    form; that is, the value of the denominator will be the
    smallest integer that can represent the ratio. For
    example, 3/6 is not normalized; 1/2 is: 3/6 can be
    normalized by dividing both top and bottom by 3, to
    produce 1/2. If the Rational is negative, then
    only the numerator is negative — the denominator is
    always positive. Clearly, the denominator can never be
    allowed to be zero.
  • Initialization of a class instance from a single int. The
    int will be the numerator of the ratio.
  • Initialization of a class instance from two ints. The two
    ints will be the numerator and the denominator of the
    ratio.
  • Instance cleanup (if necessary; if not necessary, explain
    why not)
  • Initialization of a class instance from another instance
    of class Rational.
  • Assignment of one Rational to another (how is this
    different from the above?)
  • Assignment of an int to a Rational.
  • Assignment of a Rational to a double.
  • Assignment of a double to a Rational should be prohibited
    (remember, mathematically, not all real numbers are
    rational numbers).
  • The addition of two Rationals (Rational +
    Rational) to produce another Rational.
  • The addition of a Rational and a double (Rational
    + double) to produce a double.
  • The addition of a double and a Rational (double +
    Rational) to produce a double.
  • The subtraction of two Rationals (Rational -
    Rational) to produce another Rational.
  • The subtraction of a double from a Rational
    (Rational – double) to produce a double.
  • The subtraction of a Rational from a double
    (double – Rational) to produce a double.
  • The multiplication of two Rationals (Rational *
    Rational) to produce another Rational.
  • The multiplication of a Rational with a double
    (Rational * double) to produce a double.
  • The multiplication of a double with a Rational
    (double * Rational) to produce a double.
  • The division of two Rationals (Rational /
    Rational) to produce a Rational.
  • The division of a Rational by a double (Rational /
    double) to produce a double.
  • The division of a double by a Rational (double /
    Rational) to produce a double.
  • The negation (unary minus) of a Rational
    (-Rational) to produce another Rational.
  • The unary plus of a Rational (+Rational) to
    produce another Rational.
  • A Print() member function that prints out the value of a Rational
    in its normalized form:

(3/4)	(including the parentheses) 


Strategy


It is very tempting to implement the above by writing all the
constructors, methods, operators, etc. from the start. However,
you may be surprised how much will work ‘magically’! It is not
necessary to map each of the above operations into a specific
class method (or normal function, as the case may be). I strongly
suggest that you take a minimalist approach: start with the
smallest class you can, and work from there, incrementally. See
how much that minimal class already supports, and understand
why and how it supports it
. Then add what you need, as you
discover what doesn’t work yet. As you add, keep trying to
understand how the new stuff works (or why it doesn’t work), and
how things change as you add that functionality.
Use this as
a learning experience!


Here’s a suggested approach (Yes, I do want to see
the answers to the questions!
):


  1. Create, in file rational.h,
    a class Rational, with private data members
    numerator and denominator, and a private member function Normalize(), which does basic
    checking of numerator and denominator values, and reduces
    the ratio to its normal form (i.e. numerator and
    denominator contain the smallest possible integer
    values).


    Write a public constructor to construct a Rational
    from two ints, with
    appropriate default arguments, and a public member
    function Print(), to
    print out the Rational as described earlier (Print() will help with
    debugging…). In addition, create two public access
    member functions, Numerator() and Denominator(). Here’s
    the class to start off with:


// rational.h

class Rational
{
  public:
    // Constructor
    Rational(int n = 0, int d = 1) <implement here>

    // Print out the contents of the instance
    void Print();   // Implemented in the rational.cpp file

    // Access member functions
    int Numerator()     { return m_num; }
    int Denominator()   { return m_den; }

  private:
    // Private functions
    void Normalize(); // To be implemented in the rational.cpp file

    // Private data
    int  m_num;    // numerator
    int  m_den;    // denominator
};


Your job is to fill in the <implement
here>
, and implement the Print() and Normalize() functions in the rational.cpp file.

Once you have completed this step, you should be able to
do simple construction of Rational types. Try this out
by writing a separate testrat.cpp
file, which will include the rational.h
file, and by linking the two objects produced from compiling
the rational.cpp and the testrat.cpp files.


  1. Without adding anything else to your Rational
    class, try defining an instance of class Rational,
    initializing it from another instance of class Rational.
    Does it work? If so, can you explain why?


    Now (again without adding anything to your class), try assigning
    one instance of class Rational to another. Again,
    does it work? If so, why?

  1. What about assigning other types to an instance
    of class Rational? Try assigning an int (like the integer constant 3) to a Rational. Does
    it work? If so, why, and is it reasonably efficient? If
    not, how would you make it more efficient?


    What about assigning a double
    (like 45.6) to a Rational?
    Does it work? Should it work? NO! In
    general, a double value
    cannot be expressed mathematically as an exact rational
    number. So how would you prevent this assignment from
    working?

  1. Now try assigning a Rational to a
    double. Does it work? Why?/Why not? What will you do
    about it?


    Once you have double = Rational
    assignment working, try implementing the unary minus and
    unary plus operators:

-Rational	
+Rational

What should the type of this expression be? Does it work?
What do you need to make it work?


  1. Now, try adding the binary (that is, 2-operand)
    arithmetic operations. Start with addition. First look
    at:

Rational + Rational 


What should the type of this expression be? Does it work?
What do you need to make it work?


Once you have Rational+Rational
working, try:


Rational + double


What should the type of this expression be? What in fact
happens, and why? How do you make this work?


Once you have Rational+double
working, try reversing the order of the types:


double + Rational


What should the type of this expression be? Can you
explain what you observe? What do you have to do to make this
work?


Now implement subtraction, multiplication
and division for each of these combinations of types.


  1. Make sure that a call to a constructor Rational(double, double) — for
    example:

Rational r(3.0, 5.1); 


(or any other case where such a constructor might be
called, explicitly or implicity) — fails
at compile time.


  1. At this point, most operations should be
    working. Once you have cleaned up any that are not, you
    now have three things to do:

  1. Check that each operation is indeed working,
    by thorough testing.
  2. Understand completely how each operation
    works.
  3. Determine whether each operation works
    sufficiently
    accurately
    (no undue loss of precision) and
    efficiently
    (no undue loss of performance).


How to Normalize


You will find that the following code for evaluating the greatest
common divisor
(GCD) of two integers will help you normalize
your Rational class instances. I expect you to turn this code
into a private member function of class Rational, called Normalize(). The function Normalize()takes no arguments. Here is
some code that implements a greatest common divisor
function. Change the gcd()
function so that it becomes a member function of the class –
when you make it a member function of the class, can you
eliminate the m and n parameters?.




#ifndef GCD_H
#define GCD_H
//
//	gcd.h
//

extern int gcd(int m, int n);

#endif

//
//	gcd.cpp
//
//	Compute the greatest common divisor
//	of two integers, m and n.
//	(Uses Euclid's algorithm)
//

#include "gcd.h"

int gcd(int m, int n)
{
    if ( (m == 0) || (n == 0) )
        return 1;

    if (m < 0)
        m = -m;
    if (n < 0)
        n = -n;

    // Euclid's algorithm...
    int a, b, r;

    if (n > m)
    {
        a = n;
        b = m;
    }
    else
    {
        a = m;
        b = n;
    }
    r = 1;
    while (r > 0)
    {
        r = a % b;
        a = b;
        b = r;
    }
    return a;
}

#ifdef TESTING
// This code is included for testing purposes.
// Simply define the macro TESTING, and recompile this module;
// it will be a standalone program that tests the above function.
// Once you have completed the testing, undefine the macro TESTING,
// and recompile.
//
// NOTE: Most C++ compilers provide a way of defining a macro at 
//       compile time.  If you are using a compiler from the command
//       line, there is usually a switch of some kind (-D ?) to support
//       this.  If you are using an IDE (Interactive Development 
//       Environment), you will have to learn how to define a macro at
//       compile time from within the IDE.

#include <iostream.h>

static void print_gcd(int m, int n)
{
    cout << "GCD(" << m << ',' << n ") = " << gcd(m, n) << 'n';
}

int main()
{
    print_gcd(5, 4);
    print_gcd(10, 20);
    print_gcd(3, 9);
    print_gcd(121, 99);
    return 0;
}
#endif	// TESTING


Testing your Program


The following program should work correctly with your Rational
class implementation. Test your class against this program, and
verify the results against those expected.



// 
// testrat.cpp
//

#include <iostream.h>
#include "rational.h"

int main()
{
   cout << endl << "====Check constructor with two ints:";
   cout << endl << "Rational a(3,4);";
   Rational a(3,4);
   cout << endl << "a: ";
   a.Print();

   cout << endl << "====Check constructor with one int:";
   cout << endl << "Rational b(3);";
   Rational b(3);
   cout << endl << "b: ";
   b.Print();

   cout << endl << "====Check copy constructor:";
   cout << endl << "Rational c(b);";
   Rational c(b);
   cout << endl <<"c: ";
   c.Print();

   cout << endl << "====Check Rational = Rational assignment:";
   cout << endl << "a = b;";
   a = b;
   cout << endl << "a: ";
   a.Print();

   cout << endl << "====Check Rational = int assignment:";
   cout << endl << "a = 1;";
   a = 1;
   cout << endl << "a: ";
   a.Print();

#ifdef CHECK_ERRORS // Define CHECK_ERRORS to test this
   cout << endl << "====Check Rational = double assignment:"
           " (Should not work!):";
   cout << endl << "a = 1.0;";
   a = 1.0;	// Should not compile
   cout << endl << "a: ";
   a.Print();
#endif

   cout << endl << "====Check double = Rational assignment:";
   cout << endl << "x = Rational(3,4);";
   double x;
   x = Rational(3,4);
   cout << endl << "x: " << x;

   cout << endl << "====Check Rational + Rational addition:";
   cout << endl << "c = Rational(3,4) + Rational(1,4);";
   c = Rational(3,4) + Rational(1,4);
   cout << endl << "c: ";
   c.Print();

   cout << endl << "====Check double + Rational addition:";
   cout << endl << "x = 37.25 + Rational(3,4);";
   x = 37.25 + Rational(3,4);
   cout << endl << "x: " << x;

   cout << endl << "====Check Rational + double addition:";
   cout << endl << "x = Rational(3,4) + 37.25;";
   x = Rational(3,4) + 37.25;
   cout << endl << "x: " << x;

   cout << endl << "====Check Rational - Rational subtraction:";
   cout << endl << "c = Rational(3,4) - Rational(1,4);";
   c = Rational(3,4) - Rational(1,4);
   cout << endl << "c: ";
   c.Print();

   cout << endl << "====Check double - Rational subtraction:";
   cout << endl << "x = 37.25 - Rational(3,4);";
   x = 37.25 - Rational(3,4);
   cout << endl << "x: " << x;

   cout << endl << "====Check Rational - double subtraction:";
   cout << endl << "x = Rational(3,4) - 37.25;";
   x = Rational(3,4) - 37.25;
   cout << endl << "x: " << x;

   cout << endl << "====Check Rational * Rational multiplication:";
   cout << endl << "c = Rational(3,4) * Rational(1,4);";
   c = Rational(3,4) * Rational(1,4);
   cout << endl << "c: ";
   c.Print();

   cout << endl << "====Check double * Rational multiplication:";
   cout << endl << "x = 37.25 * Rational(3,4);";
   x = 37.25 * Rational(3,4);
   cout << endl << "x: " << x;

   cout << endl << "====Check Rational * double multiplication:";
   cout << endl << "x = Rational(3,4) * 37.25;";
   x = Rational(3,4) * 37.25;
   cout << endl << "x: " << x;

   cout << endl << "====Check Rational / Rational division:";
   cout << endl << "c = Rational(3,4) / Rational(1,4);";
   c = Rational(3,4) / Rational(1,4);
   cout << endl << "c: ";
   c.Print();

   cout << endl << "====Check double / Rational division:";
   cout << endl << "x = 37.25 / Rational(3,4);";
   x = 37.25 / Rational(3,4); 
   cout << endl << "x: " << x;

   cout << endl << "====Check Rational / double division:";
   cout << endl << "x = Rational(3,4) / 37.25;";
   x = Rational(3,4) / 37.25;
   cout << endl << "x: " << x;

   cout << endl << "====Check Rational negation:";
   cout << endl << "c = -Rational(3,4);";
   c = -Rational(3,4);
   cout << endl << "c: ";
   c.Print();

   cout << endl << "====Check Rational unary plus:";
   cout << endl << "c = +Rational(3,4);";
   c = +Rational(3,4);
   cout << endl << "c: ";
   c.Print();

   cout << endl << "====Destructors about to be checked...n";
   return 0;
}


 



This page was last changed on 24 Dec 2004