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
intdata members,numeratoranddenominator, 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 should never be allowed to be zero. - Initialization of a class instance from a single
int. The value of theintwill be the numerator of the ratio. - Initialization of a class instance from two
ints. The values of the twoints 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
intto a Rational. - Assignment of a Rational to a
double. - Assignment of a
doubleto 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 adouble. - The addition of a
doubleand a Rational (double+Rational) to produce adouble. - The subtraction of two Rationals (
Rational–Rational) to produce another Rational. - The subtraction of a
doublefrom a Rational (Rational–double) to produce adouble. - The subtraction of a Rational from a
double(double–Rational) to produce adouble. - The multiplication of two Rationals (
Rational*Rational) to produce another Rational. - The multiplication of a Rational with a
double(Rational*double) to produce adouble. - The multiplication of a double with a Rational (
double*Rational) to produce adouble. - The division of two Rationals (
Rational/Rational) to produce a Rational. - The division of a Rational by a
double(Rational/double) to produce adouble. - The division of a double by a Rational (
double/Rational) to produce adouble. - 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! (Please document what you did – as much for your own benefit, as for mine.)
Here’s a suggested approach (Please provide answers to the questions!):
Step 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 member functions, Numerator() and Denominator().
Here’s the class to start off with:
//
// rational.h
// Assignment 2: Rational Class
//
// Created by Bryan Higgs on 10/28/24.
//
#ifndef rational_h
#define rational_h
class Rational
{
public:
// Constructor
Rational(int num = 0, int den = 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
};
#endif /* rational_h */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.
Step 2
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?
Step 3
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?
Step 4
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?
Step 5
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.
Step 6
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 implicitly) — fails at compile time.
Step 7
At this point, most operations should be working. Once you have cleaned up any that are not, you now have three things to do:
- Check that each operation is indeed working, by thorough testing.
- Understand completely how each operation works.
- 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.
//
// gcd.cpp
// Assignment 2: Rational Class
//
// Created by Bryan Higgs on 10/31/24.
//
//
// Compute the greatest common divisor
// of two integers, m and n.
// (Uses Euclid's algorithm)
//
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>
static void print_gcd(int m, int n)
{
std::cout << "GCD(" << m << ',' << n << ") = " << gcd(m, n) << '\n';
}
int main(int argc, const char * argv[])
{
print_gcd(5, 4);
print_gcd(10, 20);
print_gcd(3, 9);
print_gcd(121, 99);
return 0;
}
#endif // TESTINGChange 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?.
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
// Assignment 2: Rational Class
//
// Created by Bryan Higgs on 10/31/24.
//
#include <iostream>
#include "rational.h"
//
// Main program to test the Rational class
//
int main(int argc, const char * argv[])
{
std::cout << std::endl << "====Check constructor with two ints:";
std::cout << std::endl << "Rational a(3,4);";
Rational a(3,4);
std::cout << std::endl << "a: ";
a.Print();
std::cout << std::endl << "====Check constructor with one int:";
std::cout << std::endl << "Rational b(3);";
Rational b(3);
std::cout << std::endl << "b: ";
b.Print();
std::cout << std::endl << "====Check copy constructor:";
std::cout << std::endl << "Rational c(b);";
Rational c(b);
std::cout << std::endl <<"c: ";
c.Print();
std::cout << std::endl << "====Check Rational = Rational assignment:";
std::cout << std::endl << "a = b;";
a = b;
std::cout << std::endl << "a: ";
a.Print();
std::cout << std::endl << "====Check Rational = int assignment:";
std::cout << std::endl << "a = 1;";
a = 1;
std::cout << std::endl << "a: ";
a.Print();
#ifdef CHECK_ERRORS // Define CHECK_ERRORS to test this
std::cout << std::endl << "====Check Rational = double assignment:"
" (Should not work!):";
std::cout << std::endl << "a = 1.0;";
a = 1.0; // Should not compile
std::cout << std::endl << "a: ";
a.Print();
#endif
std::cout << std::endl << "====Check double = Rational assignment:";
std::cout << std::endl << "x = Rational(3,4);";
double x;
x = Rational(3,4);
std::cout << std::endl << "x: " << x;
std::cout << std::endl << "====Check Rational + Rational addition:";
std::cout << std::endl << "c = Rational(3,4) + Rational(1,4);";
c = Rational(3,4) + Rational(1,4);
std::cout << std::endl << "c: ";
c.Print();
std::cout << std::endl << "====Check double + Rational addition:";
std::cout << std::endl << "x = 37.25 + Rational(3,4);";
x = 37.25 + Rational(3,4);
std::cout << std::endl << "x: " << x;
std::cout << std::endl << "====Check Rational + double addition:";
std::cout << std::endl << "x = Rational(3,4) + 37.25;";
x = Rational(3,4) + 37.25;
std::cout << std::endl << "x: " << x;
std::cout << std::endl << "====Check Rational - Rational subtraction:";
std::cout << std::endl << "c = Rational(3,4) - Rational(1,4);";
c = Rational(3,4) - Rational(1,4);
std::cout << std::endl << "c: ";
c.Print();
std::cout << std::endl << "====Check double - Rational subtraction:";
std::cout << std::endl << "x = 37.25 - Rational(3,4);";
x = 37.25 - Rational(3,4);
std::cout << std::endl << "x: " << x;
std::cout << std::endl << "====Check Rational - double subtraction:";
std::cout << std::endl << "x = Rational(3,4) - 37.25;";
x = Rational(3,4) - 37.25;
std::cout << std::endl << "x: " << x;
std::cout << std::endl << "====Check Rational * Rational multiplication:";
std::cout << std::endl << "c = Rational(3,4) * Rational(1,4);";
c = Rational(3,4) * Rational(1,4);
std::cout << std::endl << "c: ";
c.Print();
std::cout << std::endl << "====Check double * Rational multiplication:";
std::cout << std::endl << "x = 37.25 * Rational(3,4);";
x = 37.25 * Rational(3,4);
std::cout << std::endl << "x: " << x;
std::cout << std::endl << "====Check Rational * double multiplication:";
std::cout << std::endl << "x = Rational(3,4) * 37.25;";
x = Rational(3,4) * 37.25;
std::cout << std::endl << "x: " << x;
std::cout << std::endl << "====Check Rational / Rational division:";
std::cout << std::endl << "c = Rational(3,4) / Rational(1,4);";
c = Rational(3,4) / Rational(1,4);
std::cout << std::endl << "c: ";
c.Print();
std::cout << std::endl << "====Check double / Rational division:";
std::cout << std::endl << "x = 37.25 / Rational(3,4);";
x = 37.25 / Rational(3,4);
std::cout << std::endl << "x: " << x;
std::cout << std::endl << "====Check Rational / double division:";
std::cout << std::endl << "x = Rational(3,4) / 37.25;";
x = Rational(3,4) / 37.25;
std::cout << std::endl << "x: " << x;
std::cout << std::endl << "====Check Rational negation:";
std::cout << std::endl << "c = -Rational(3,4);";
c = -Rational(3,4);
std::cout << std::endl << "c: ";
c.Print();
std::cout << std::endl << "====Check Rational unary plus:";
std::cout << std::endl << "c = +Rational(3,4);";
c = +Rational(3,4);
std::cout << std::endl << "c: ";
c.Print();
std::cout << std::endl << "====Destructors about to be checked...\n";
return 0;
}