Rational numbers with encapsulation =================================== This example extends the previous Rational-number project by introducing constructors, private member variables, and member functions. The arithmetic and comparison operations are essentially unchanged. The important difference is that the Rational class now controls how a rational number is created and how its numerator and denominator can be accessed. The problem with public member variables ======================================== Previously, the numerator and denominator were public. Consequently, a user could write Rational p{ 3, 4 }; p.denom_ = 0; and turn a valid rational number into an invalid one. The user could also create an invalid object immediately: Rational p{}; which previously initialized both the numerator and denominator to zero. We would prefer the class itself to guarantee that every Rational object represents a valid rational number. Private member variables ======================== The numerator and denominator are now private: private: int numerator_; int denominator_; Code outside the class cannot directly access or modify these member variables. Instead, their values can be queried using the member functions p.numer() p.denom() Neither function allows the user to change the stored value. A constructor ============= A constructor is a special member function that is called when an object is created. Our constructor is declared as Rational( int numerator = 0, int denominator = 1 ); Thus, Rational p{ 3, 4 }; constructs the rational number 3/4, while Rational p{}; uses the default arguments and constructs 0/1. The constructor asserts that the denominator is not zero. Normal form =========== The constructor also places every rational number into normal form. We require that 1. the denominator is positive, and 2. the numerator and denominator have no common divisor greater than one. For example, Rational p1{ -1, 2 }; Rational p2{ 100, -200 }; Rational p3{ -47, 94 }; are all stored internally as -1/2 Consequently, once a Rational object has been constructed, we know that it is in normal form. The greatest common divisor =========================== To put a rational number into normal form, the constructor calculates the greatest common divisor (GCD) of the numerator and denominator and divides both by it. The class therefore contains a private helper function: static int gcd( int m, int n ); This function is private because it is an implementation detail of the Rational class. It is static because calculating a GCD does not require a particular Rational object. Maintaining normal form ======================= Arithmetic operations create new Rational objects. For example, adding two rational numbers ultimately constructs a result similar to Rational{ numerator_*q.denominator_ + q.numerator_*denominator_, denominator_*q.denominator_ } The constructor then automatically reduces that result to normal form. Thus, the code for every arithmetic operation does not need to separately normalize its result. The constructor guarantees that every newly created Rational object is normalized. Member functions ================ Many operations are now member functions. For example, p.inverse() p.abs() p.real() p.to_string() operate on the object p. Similarly, p + q uses the member operator p.operator+( q ) and p == q uses p.operator==( q ) This is another benefit of a class: the data and the operations on that data can be grouped together. Why are some operators still free functions? ============================================= Not every operator can naturally be a Rational member function. For example, p + 3 can use a member function of p, but 3 + p cannot because the left operand is an int, not a Rational object. Consequently, operators such as 3 + p 3 - p 3*p 3/p remain free functions. The stream insertion operator also remains a free function: std::cout << p; Its left operand is std::cout rather than the Rational object. Encapsulation ============= The combination of private data and controlled member functions is an example of encapsulation. The user can create and use Rational objects without being able to directly corrupt their internal representation. The class is responsible for maintaining its own rules: denominator_ > 0 and the numerator and denominator are always reduced by their GCD. This makes the rest of the implementation simpler because functions operating on Rational objects can rely on these rules. Files ===== Rational.hpp Contains the Rational class definition, including the constructor, public member functions, private member variables, and declarations of the remaining free functions. Rational.cpp Contains the definitions of the constructor, member functions, operators, and private GCD helper function. main.cpp Demonstrates construction, normalization, arithmetic, comparisons, member functions, and output. Compiling ========= Compile both main.cpp and Rational.cpp. For example: g++ -std=c++17 -Wall -Wextra -pedantic main.cpp Rational.cpp