A rational number class with private member variables

The following is an exercise in object-oriented programming intended for teaching purposes. We will consider a class representing rational numbers, that is, numbers that can be expressed as the quotient of two integers, where the denominator is not zero.

The Rational class supports the usual arithmetic operations of addition, subtraction, multiplication, and division, together with comparisons, unary plus and minus, absolute values, multiplicative inverses, and conversion to a floating-point value. Rational numbers can also be combined and compared directly with integers.

Private member variables

Unlike some of our earlier examples, this class uses private member variables to store the numerator and denominator. This is an important step in object-oriented programming: rather than allowing programs to access and modify the internal representation directly, we require them to interact with the object through its public member functions.

There are several reasons for this restriction:

  1. The denominator must never be zero. A rational number with a zero denominator is undefined. By making the denominator private, we prevent a program from accidentally assigning zero to it. The constructor checks that the denominator is nonzero.

  2. Every rational number is kept in normal form. The numerator and denominator must have a greatest common divisor of one, and the denominator must always be positive. If the rational number is negative, the negative sign is associated with the numerator. For example, 6/8 is stored as 3/4, 2/-6 as -1/3, and 0/5 as 0/1.

  3. Accidental modification is prevented. If the numerator and denominator were public, a program could assign arbitrary values to either member variable, possibly violating the conditions above. Private member variables ensure that changes to a rational number occur only through operations provided by the class.

These conditions are called class invariants: properties that must remain true for every valid object throughout its lifetime. The constructor establishes these invariants, and the operations of the class must preserve them.

Construction and arithmetic

The constructor accepts a numerator and denominator, verifies that the denominator is nonzero, moves any negative sign to the numerator, and divides both integers by their greatest common divisor. Consequently, every newly constructed rational number is in normal form.

One useful feature of this design is that arithmetic operators can construct and return new Rational objects without having to repeat the normalization process. For example, adding 1/6 and 1/3 produces the intermediate numerator and denominator 9/18, but constructing the result automatically reduces it to 1/2.

Because every rational number is in normal form, checking whether two rational numbers are equal is particularly simple: their numerators and denominators must be identical.

Public interface

Although the numerator and denominator are private, their values can still be examined using the public member functions numer() and denom(). These functions return the corresponding values without allowing the caller to modify the private member variables.

The class also demonstrates operator overloading. Expressions such as p + q, p/q, p*3, and 2 - p can be written naturally, where p and q are rational numbers. Some operators are member functions, while others must be non-member functions because their left-hand operand is not a Rational object.

Implementation

The C++ implementation separates the declarations in Rational.hpp from the definitions in Rational.cpp. A separate program demonstrates the construction and use of rational numbers.

Rational class source files

Comments and disclaimer

The purpose of this exercise is not to develop a production-quality rational arithmetic library. In particular, arithmetic on the underlying integers may overflow, and invalid operations are checked using assertions rather than exceptions.

Instead, the purpose is to demonstrate how private member variables, constructors, and public operations work together to maintain class invariants. By controlling access to the internal representation, we can ensure that every valid Rational object remains in normal form and represents a mathematically valid rational number.

This exercise and its underlying ideas are those of Douglas Wilhelm Harder. The C++ implementation and accompanying documentation were developed with the assistance of ChatGPT.