530 likes | 645 Views
Operator Overloading Version 1.0. Objectives. At the end of this lesson, students should be able to: Write programs that correctly overload operators Describe how to overload a unary operator Tell how the compiler differentiates between pre- and post
E N D
Objectives At the end of this lesson, students should be able to: Write programs that correctly overload operators Describe how to overload a unary operator Tell how the compiler differentiates between pre- and post Describe how to overload a binary operator Explain when to use member vs non-member functions Explain when it is necessary to use friend functions Demonstrate how to return values when overloading operators Explain why operator overloading is useful Explain what the compiler generates when it sees an overloaded operator Demonstrate how constructors and overloaded cast operator are used to do conversions
Operator Overloading In C++, operators are nothing more than functions, written with a different, but more familiar syntax.
We use operators to express fundamental operations in a concise and readable way. For example, y = x + y *z; is much easier to read and write than y.assign (x.add (y.multiply(z)));
The standard operators in C++ work on all of the basic data types. However, they do not work on user defined data types. To add two objects of the Length class, we could write a function called add, so we could write lengthThree = lengthTwo.add (lengthOne); But, this is awkward. Wouldn’t it be nicer to be able to write lengthThree = lengthTwo + lengthOne;
We can do this if we overload the + operator so that it works with objects of the Length class.
Define the Length class as … class Length { public: Length( ); private: int feet; int inches; }; The Length class keeps track of a length measurement in feet and inches.
To overload the + operator so that we can add two Length objects together, we write a function of the form: Length operator+ (const Length&); the name of the function is operator+ in this case the function returns a Length object. the function takes a const reference to a Length object as a parameter.
Now, if we write lengthThree = LengthTwo + LengthOne; The compiler looks in the Length class for a function with the prototype Length ::operator+ (const Length&);
LengthThree = lengthTwo + lengthOne and generates code just as if we had written lengthThree = lengthTwo.operator+ (lengthOne); the left hand operand becomes the calling object. the right hand operand becomes the parameter in the function call.
All of the C++ operators can be overloaded except for the following: . .* :: ?: sizeof Because of the complexities involved, the overloading of &&, ||, and the comma operator is discouraged.
Key points to remember You cannot change the precedence rule by overloading operators. You cannot change the associativity of an operator by overloading it. It is not possible to create new operators. You cannot change the way that the operators work on the basic data types. You cannot change the “arity” of an operator by overloading it.
Overloading Unary Functions It certainly ought to be possible to overload the increment and decrement operators for the Length class. For example, we would probably like to be able to do the following for a length object lengthOne: lengthOne++;
The “makes sense” Rule A programmer can clearly define what to do inside of the function that overloads an operator. However, it is just good programming practice to be sure that the function makes good sense.
So … what does it mean to increment a Length?
Let’s choose the option of adding one inch to the current Length value. This points out a complexity that we may not have thought of before. Suppose we have a Length object whose value is 3 feet and 11 inches. What happens when we increment this object. Obviously we want the result to be 4 feet. This complicates the code we would write, because we always have to check to see if the addition of one inch results in going to a new number of feet.
Let’s simplify the problem by re-defining the Length class and take advantage of data hiding. class Length { public: Length( ); private: int inches; }; Because we only keep track of inches, we don’t have to worry here (or in any other arithmetic operation) about handling the overflow from inches to feet.
Now the code for the operator++ function would look something like: const Length& Length::operator++( ) { ++inches; return *this; } when written as a member function, functions that overload unary operators take no parameters. the function works on the calling object. In this case, the inches data member of the calling object is incremented. we return the calling object (by reference).
in this case, the result of incrementing lengthOne is used as the right hand operand for the assignment operator. So, the increment operator must return a Length object. Return Types In the increment example, why did we return a Length object, and why by reference? We pass by reference for efficiency! In any expression, the result of some operation may be used as an operand for another operator. Consider the expression cout << ++lengthOne;
More Complications! There are a couple of other issues to consider in this case. 1. How do we differentiate between a pre- and a post-increment? 2. Should we write the operator++ function as member function or as a non-member function?
pre- or post-increment The code we just examined is for the pre-increment operator. To tell the compiler that we want the function to be for the post-increment operator, we put a dummy parameter in the function prototype, as : Length operator++ ( int ); this parameter is never actually used. It is simply a flag to the compiler that this function is overloading the post-increment operator.
Length Length::operator++ (int) { Length tl = *this; ++inches; return tl; } What’s going on here??? Why is the return different
the compiler invokes the operator++(int) function Length Length::operator++ (int) { Length tl = *this; ++inches; return tl; } inches 5 Length object tl is local to the function myLength++; inches 5 6 calling object, myLength a copy of the object tl is put on the stack and returned to the calling program. Why can’t we return a reference here?
cout << myLength++; the result is that the copy of the original object (with the original value of inches) is sent to cout. But, the inches value of myLength is equal to 6. The effect is that myLength gets incremented after (post-increment) the << operator is executed.
Member vs. Non-member When overloading ( ), [ ], ->, or any of the assignment operators, the overloading operator function must be written as a member function. For all other operators, we may write the function as a member or a non-member function – your choice. If the lefthand operand is a basic data type, or an object of a different class than the operator’s class, then the operator overloading function must be a non-member function. Non-member functions can be written as friend functions.
Friends Friend functions are non-member functions that have all of the privileges of member functions. In particular, a friend function can access private data in the class that it is a friend of.
Friendship must be given! class Length { public: Length( ); friend Length& operator++ (Length& t); private: int inches; }; the keyword friend tells the compiler that the function operator++ is a friend of the class. It is a non-member function. The function has access to the private data inches.
because it is a non-member function, we must pass the object to be worked on as a parameter. the function goes in a .cpp file. Note that there is no scope resolution operator nor a class name because the function does not belong to the class. Length operator++ (Length& t) { t.inches++; return t; } the code inside the function can directly access the inches data member, even though it is private. don’t include the keyword friend here. The function cannot claim it is a friend. This is done in the class definition.
Object Oriented purists don’t like friend functions because they break the rules of encapsulation and data hiding. If you want to be pure, provide accessor functions in the class and invoke them from the non-member function. However, this is less efficient than using a friend function.
Friend Classes A class can be a friend of another class. When a class is declared as a friend, all of its member functions can access the private data of the class it is a friend of.
Overloading Binary Operators A binary operator takes two operands. We will refer to these as the left-hand operand and the right-hand operand. operator a = b + c; right-hand operand left-hand operand
As a Member Function functions that overload binary operators, written as member functions, take one parameter, the right-hand operand. the result of adding two Length objects ought to be a Length object. Length Length::operator+ (const Length& rh) { Length tLen; tLen.inches = inches + rh.inches; return tLen; } this value comes from the calling, or implicit object, the left-hand operand.
for a, b, and c all Length objects … operator a = b + c; b. operator+ (c); The compiler generates the function invocation … right-hand operand left-hand operand
the operator+ function is called to evaluate the right hand side of the equation. Nameless Temporary Objects In the evaluation of the expression a = b + c; finally the assignment is done. By default, each data member of the nto is copied into the corresponding data member of the object a. a Length object is returned on the stack. This object has no name, and is oftentimes called a nameless temporary object (nto). after completing the assignment, the nto is removed from the stack and no longer exists.
Commutative Operators + is a commutative operator. That is, b + c; is the same as c + b;
But what if one operand is a basic data type, for example, an integer. This probably makes sense, since, for example, we can think of adding an integer, like 5, to a Length. To make the operator commutative, we should be able to write Length c = myLength +5; as well as Length c = 5 + myLength; but … how do we write the function in this case?
Rule of thumb: If the piece of data that you would normally send the message to (the left-hand operand) is a basic data type, then you have to overload the operator by writing a non-member function!
As a Non-member Function! functions that overload binary operators take two operands when written as a non- member function. Length operator+ ( int lh, const Length& rh) { Length tLength; tLength.inches = lh + rh.inches; return tLength; }
Overloading << and >> << and >> are binary operators where the left-hand operator is always of a different class than the one for which we are overloading the operator. For example, cout << myLength; so .. we must write these as non-member functions.
Because we want to represent a Length as feet and inches externally, we must do some conversions. As a result, the function to overload the << operator might look like: out must be passed by reference because the ostream class has no copy constructor. ostream &operator<< (ostream &out, const Length &rh) { int ft = rh.inches/12; int in = rh.inches %12; out << ft << “ft., “ << in << “in”; return out; } convert inches to feet and inches. the stream is returned so that we can cascade the << operator.
Cascading << out << ft << “ft., “ << in << “in”; this expression is evaluated first, and returns the stream out. The entire expression may now be thought of as out << “ft., “ << in << “in”; where out already contains 5 out
out << “ft., “ << in << “in”; this expression is evaluated next, and returns the stream out. The entire expression may now be thought of as out << in << “in”; where out already contains 5 ft. out
out << in << “in”; this expression is evaluated next, and returns the stream out. The entire expression may now be thought of as out << “in”; where out already contains 5 ft. 3 out
out << “in”; finally this expression is evaluated and returns the stream out. The return value is not used in this last instance. out now contains 5 ft. 3 in. out
Stream Extraction Overloading the Stream Extraction operator is similar. Here, we have to decide how the user will enter in the length. In this example, I have assumed that the user will enter the number of feet, followed by white space, and then the number of inches (I would probably prompt the user to enter the data this way).
istream &operator>> (istream &input, Length &rh) { int ft, in; input >> ft; input >> in; rh.inches = in + 12 * ft; return input; }
Conversions and Overloading When converting from a basic data type to a user defined class, the compiler looks for a constructor in the class that takes the base data type as a parameter. Length::Length (const float r) { inches = r * 12; }
Now, if the programmer writes myLength = 5.3; the compiler invokes the constructor to create a nameless temporary object (nto). The assignment from the nto to the object myLength is then performed and the nto is thrown away. Note: 5.3 is in feet!
When converting from a user defined class to a base data type, the compiler looks for a function that overloads the cast operator. Length::operator float( ) { int ft, in; ft = inches/12; in = inches %12; float r = (float)in/12.0; return ft + r; } notice that no return type is declared. The return type is derived from the type of cast being done.