The Rational Class header file, a better version

This class is essentially the same class as the simple Rational class, but this example
  • Is documented appropriately
  • Introduces overloading operators
  • Introduces nonmember friend functions

    The header file   --   the whole thing: rat2.h

    This header file is similar to the simple Rational class example. The preprocessor statements and the using statement at the beginning of the .h file are identical to the simple Rational .h file (except a different identifier, RAT2_H, to match the filename is used):
    #ifndef RAT2_H
    #define RAT2_H
    #include < iostream >
    using namespace std;
    
    Consider the class definition. The private members are the same: numerator, denominator, and the reduce() function. And notice that the constructor is identical to the simple Rational class, having two parameters, both with default values. Temporarily skip over the friend declarations. Let's consider the arithmetic operators first: operator+ , operator- , etc. first.
       Rational operator+(const Rational &) const;
       Rational operator-(const Rational &) const;
       Rational operator*(const Rational &) const;
       Rational operator/(const Rational &) const;
    

    The operator+ function
    This is how you overload operators, i.e., making operators be function calls. You can overload all the operators except the dot operator, the .*, the ?:, and sizeof. You need to be very careful when overloading operators that you don't make an operator perform an unintuitive task. And since you cannot change the precedence of operators, you should not overload &&, ||, and the comma operator. You also cannot change the order of operation (left to right, or right to left).

    The name of the function is operator+. The word operator is a keyword. This function performs in an identical manner (exact same code) to add of the simple Rational class.

    This is used by a client program, e.g., main, as something like
      z = x + y;

    Consider only the righthand side. The code   x + y   is identical to   x.operator+(y)   to make it look like traditional object/function call.

    The operator+ is passed a Rational object to add to the current object (this object) and returns a Rational object. As seen before, the parameter is fake pass-by-value, passing a reference, but casting it to a constant object within this function.


    When const is in a parameter
    Because the parameter is a constant object within the function, you can pass in a constant object as you know it will not be changed in this routine. That means it will compile fine with something that seems very reasonable like

      const Rational c(1,2);
      Rational x(7,8), y;
      y = x + c;


    Without the parameter being a const,   x + c   wouldn't compile.


    When const is at the end
    So what does that const at the end of the prototype do? It allows the current object to be a constant object. Consider

      const Rational c(1,2);
      Rational x(7,8), y;
      y = c + x;


    Without that const at the end,   c + x   wouldn't compile. It doesn't mean that the first operand must be a constant, it means it is allowed to be a constant. Similarly with the const for the parameter.


    The other overloaded operator functions and returning a reference
    The rest of the arithmetic operators are similar to the   operator+  . The boolean comparison operators are also similar except that they return a bool. Remember they are used in the same way. If you have code something like
       if (x == y) 
          ...
    
    that this is the same as
       if (x.operator==(y)) 
          ...
    
    Notice that the assignment operators are slightly different:
       Rational& operator+=(const Rational &);
    
    It makes sense that there is no const at the end, because you are changing the current object. The code
       y += x; 
    
    is the same as
       y.operator+=(x); 
    
    so the current object, y, is getting changed. The return type is   Rational&   (returning a reference to a Rational object) primarily because the thing you're changing exists outside the function, i.e., isn't local, and it's more efficient to simply copy back the address than make a copy of the whole object (which is what happens when you return a Rational object). It's similar to fake pass-by-value when you pass in the address/reference instead of making a copy of the object.

    You can NEVER return the address of anything local in a function because it no longer exists for your use when you leave the function. Sometimes you have to pass back a copy, like in operator+ . A local object was created that is the sum of the two Rational objects and that's what gets returned.


    Nonmember friend functions and overloading << and >>
    There are times when nonmember functions need to access private members. You need to be very careful when you do this because you're violating object-oriented principles such as information hiding. In fact, one of the only times I recommend it is when you're overloading the   operator<<   and   operator>>   functions.

    To be a member function, when you overload an operator, the left operand must be a member of the class. But consider a typical output statement:
       cout << x;
    
    which is the same as
       cout.operator<<(x);
    
    The left operand will always be some stream object like cin or cout and will never be a Rational object or whatever your class is. So   operator<<   and   operator>>   can never be members of the class. And yet they need to do input and output which always involves private data members. Sure you could write lots of accessor functions or write some public function that does the same thing and call it, but that's not very elegant.

    So the solutionn is to make the   operator<<   and   operator>>   functions friend functions:
    friend ostream& operator<<(ostream&, const Rational&);
    friend istream& operator>>(istream&, Rational&);
    
    The keyword friend says that the functions are friends of the class. And friends get to access the class' private members. Yes, no non-humorous way to say that. Friends can access your private members.

    Friendship is not commutative. If I'm your friend, doesn't mean you are my friend. All friendship must be declared. Think of the Rational class as saying:   The   operator<<   and   operator>>   functions are my friends.

    When you write your own   operator<<   and   operator>> , the prototypes will look almost identical to the Rational ones. The first parameter and the return type will always be an ostream& or istream& . The second parameter will be the type of the class you are writing.

    Classes can be friends of other classes too, but that is not commonly used as it violates OO principles.