C++ has a number of built-in types, including int, real, char, and so forth. Each of these has a number of built-in operators, such as addition (+) and multiplication (*). C++ enables you to add these operators to your own classes as well.
In order to explore operator overloading fully, Listing 14.1 creates a new class, Counter. A Counter object will be used in counting (surprise!) in loops and other applications where a number must be incremented, decremented, or otherwise tracked.
Unlike a real, built-in, honest, red-blooded int, the counter object cannot be incremented, decremented, added, assigned, or otherwise manipulated. In exchange for this, it makes printing its value far more difficult!
Operator overloading provides much of the functionality that would otherwise be missing in user-defined classes such as Counter. When you implement an operator for your class, you are said to be “overloading” that operator. Listing 14.2 illustrates how to overload the increment operator.
On line 36 you can see that the increment operator is invoked |
++i;
This is interpreted by the compiler as a call to the implementation of operator++ shown on lines 22–26, which increments its member variable itsValue and then dereferences the this pointer to return the current object. This provides a Counter object to be assigned to a. If the Counter object allocated memory, it would be important to override the copy constructor. In this case, the default copy constructor works fine.
Note that the value returned is a Counter reference, thereby avoiding the creation of an extra temporary object. It is a const reference because the value should not be changed by the function using this Counter.
What if you want to overload the postfix increment operator? Here the compiler has a problem. How is it to differentiate between prefix and postfix? By convention, an integer variable is supplied as a parameter to the operator declaration. The parameter's value is ignored; it is just a signal that this is the postfix operator.
Before you can write the postfix operator, you must understand how it is different from the prefix operator. To review, prefix says increment and then fetch while postfix says fetch and then increment.
Therefore, while the prefix operator can simply increment the value and then return the object itself, the postfix must return the value that existed before it was incremented. To do this, a temporary object must be created. This temporary object will hold the original value while the value of the original object is incremented. The temporary object is returned, however, because the postfix operator asks for the original value, not the incremented value.
Let's go over that again. If you write
a = x++;
Then x is 5, after this statement a is 5, but x is 6. The value in x is returned and assigned to a, which increases the value of x. If x is an object, its postfix increment operator must stash away the original value (5) in a temporary object, increment x's value to 6, and then return that temporary object to assign its value to a.
Note that because the temporary object is being returned, it must be returned by value and not by reference, because it will go out of scope as soon as the function returns.
Listing 14.3 demonstrates the use of both the prefix and the postfix operators.
The value of i is 0 The value of i is 1 The value of i is 2 The value of a: 3 and i: 3 The value of a: 4 and i: 4
The goal is to be able to declare two Counter variables and then add them, as in this example:
Counter varOne, varTwo, varThree; VarThree = VarOne + VarTwo;
Once again, you could start by writing a function, Add(), that would take a Counter as its argument, add the values, and then return a Counter with the result. Listing 14.4 illustrates this approach.
In order to create varThree without having to initialize a value for it, a default constructor is required. The default constructor initializes itsVal to 0, as shown on lines 23–25. Because varOne and varTwo need to be initialized to a non-zero value, another constructor is created, as shown on lines 19–21. Another solution to this problem is to provide the default value 0 to the constructor declared on line 8.
The Add() function itself is shown on lines 29–32. It works, but its use is unnatural. Overloading the operator + is a more natural use of the Counter class. Listing 14.5 illustrates this.
When the addition operator is invoked on line 33 |
varThree = varOne + varTwo;
the compiler interprets this as if you had written
varThree = varOne.operator+(varTwo);
and invokes the operator+ method declared on line 12 and defined on lines 25–28. Compare these with the declaration and definition of the Add() function in the previous listing; they are nearly identical. The syntax of their use, however, is quite different. It is more natural to say this:
varThree = varOne + varTwo;
than it is to say this:
varThree = varOne.Add(varTwo);
Not a big change, but enough to make the program easier to use and understand.
Operators on built-in types (such as int) cannot be overloaded. The precedence order cannot be changed, and the arity of the operator, that is whether it is unary, binary, or trinary cannot be changed. You cannot make up new operators, so you cannot declare ** to be the “power of” operator. |
Operator overloading is one of the aspects of C++ most overused and abused by new programmers. It is tempting to create new and interesting uses for some of the more obscure operators, but these invariably lead to code that is confusing and difficult to read.
Of course, making the + operator subtract and the * operator add can be fun, but no professional programmer would do that. The greater danger lies in the well-intentioned but idiosyncratic use of an operator—using + to mean concatenate a series of letters, or / to mean split a string. There is good reason to consider these uses, but there is even better reason to proceed with caution. Remember, the goal of overloading operators is to increase usability and understanding.
Remember that the compiler provides a default constructor, destructor, and copy constructor. The fourth and final function that is supplied by the compiler, if you don't specify one, is the assignment operator (operator=()).
This operator is called whenever you assign to an object. For example:
CAT catOne(5,7); CAT catTwo(3,4); // ... other code here catTwo = catOne;
Here, catOne is created and in the constructor, the parameters (5,7) will be used to initialize the member variables itsAge and itsWeight respectively. catTwo is then created, assigned, and initialized with the values 3 and 4. Finally, you see the assignment operator (operator =)
catTwo = catOne;
The result of this assignment is that catTwo's itsAge and itsWeight values are assigned the values from catOne. Thus, after this statement executes, catTwo.itsAge will have the value 5 and catTwo.itsWeight will have the value 7.
Note, in this case the copy constructor is not called. catTwo already exists; there is no need to construct it, and so the compiler is smart enough to call the assignment operator.
In Hour 13, “Advanced Functions,” I discussed the difference between a shallow (member-wise) copy and a deep copy. A shallow copy just copies the members, and both objects end up pointing to the same area on the heap. A deep copy allocates the necessary memory. You saw an illustration of the default copy constructor in Figure 13.1; refer back to that figure if you need to refresh your memory.
You see the same issue here with assignment as you did with the copy constructor. There is an added wrinkle with the assignment operator, however. The object catTwo already exists and already has memory allocated. That memory must be deleted if there is to be no memory leak. So the first thing you must do when implementing the assignment oper ator is delete the memory assigned to its pointers. But what happens if you assign catTwo to itself, like this:
catTwo = catTwo;
No one is likely to do this on purpose, but the program must be able to handle it. More important, it is possible for this to happen by accident when references and dereferenced pointers hide the fact that the object's assignment is to itself.
If you did not handle this problem carefully, catTwo would delete its own memory allocation. Then, when it was ready to copy in the memory from the right-hand side of the assignment, it would have a very big problem: The memory would be gone.
To protect against this, your assignment operator must check to see if the right-hand side of the assignment operator is the object itself. It does this by examining the this pointer. Listing 14.6 shows a class with an assignment operator.
frisky's age: 5 Setting frisky to 6; whiskers' age: 5 copying frisky to whiskers... whiskers' age: 6
Listing 14.6 brings back the CAT class, and leaves out the copy constructor and destructor to save room. On line 12, the assignment operator is declared, and on lines 28–39, it is defined.
On line 30, the current object (the CAT being assigned to) is tested to see if it is the same as the CAT being assigned. This is done by checking if the address of rhs is the same as the address stored in the this pointer.
Of course, the equality operator (==) can be overloaded as well, enabling you to determine for yourself what it means for your objects to be equal.
On lines 32–35 the member variables are deleted and then re-created on the heap. While this is not strictly necessary, it is good, clean programming practice and will save you from memory leaks when working with variable length objects that do not overload their assignment operator. |
18.226.104.27