Day 10. Working with Advanced Functions

On Day 5, “Organizing into Functions,” you learned the fundamentals of working with functions. Now that you know how pointers and references work, you can do more with functions.

Today, you will learn

• How to overload member functions

• How to overload operators

• How to write functions to support classes with dynamically allocated variables

Overloaded Member Functions

On Day 5, you learned how to implement function polymorphism, or function overloading, by writing two or more functions with the same name but with different parameters. Class member functions can be overloaded as well, in much the same way.

The Rectangle class, demonstrated in Listing 10.1, has two DrawShape() functions. One, which takes no parameters, draws the rectangle based on the class’s current values. The other takes two values, width and length, and draws the rectangle based on those values, ignoring the current class values.

Listing 10.1. Overloading Member Functions


1:  //Listing 10.1 Overloading class member functions
2:  #include <iostream>
3:
4:  // Rectangle class declaration
5:  class Rectangle
6:  {
7:    public:
8:       // constructors
9:       Rectangle(int width, int height);
10:       ~Rectangle(){}
11:
12:       // overloaded class function DrawShape
13:       void DrawShape() const;
14:       void DrawShape(int aWidth, int aHeight) const;
15:
16:    private:
17:       int itsWidth;
18:       int itsHeight;
19:  };
20:
21:  //Constructor implementation
22:  Rectangle::Rectangle(int width, int height)
23:  {
24:      itsWidth = width;
25:      itsHeight = height;
26:  }
27:
28:
29:  // Overloaded DrawShape - takes no values
30:  // Draws based on current class member values
31:  void Rectangle::DrawShape() const
32:  {
33:      DrawShape( itsWidth, itsHeight);
34:  }
35:
36:
37:  // overloaded DrawShape - takes two values
38:  // draws shape based on the parameters
39:  void Rectangle::DrawShape(int width, int height) const
40:  {
41:      for (int i = 0; i<height; i++)
42:      {

43:        for (int j = 0; j< width; j++)
44:        {
45:           std::cout << "*";
46:        }
47:      std::cout << std::endl;
48:      }
49:  }
50:
51:  // Driver program to demonstrate overloaded functions
52:  int main()
53:  {
54:     // initialize a rectangle to 30,5
55:     Rectangle theRect(30,5);
56:     std::cout << "DrawShape():" << std::endl;
57:     theRect.DrawShape();
58:     std::cout << " DrawShape(40,2):" << std::endl;
59:     theRect.DrawShape(40,2);
60:     return 0;
61:  }

Image


DrawShape():
******************************
******************************
******************************
******************************
******************************

DrawShape(40,2):
************************************************************
************************************************************

Image

Listing 10.1 represents a stripped-down version of the Week in Review project from Week 1. The test for illegal values has been taken out to save room, as have some of the accessor functions. The main program has been stripped down to a simple driver program, rather than a menu.

The important code, however, is on lines 13 and 14, where DrawShape() is overloaded. The implementation for these overloaded class methods is on lines 31–49. Note that the version of DrawShape() that takes no parameters simply calls the version that takes two parameters, passing in the current member variables. Try very hard to avoid duplicating code in two functions. Otherwise, keeping them in sync when changes are made to one or the other will be difficult and error-prone.

The driver program on lines 52–61 creates a rectangle object and then calls DrawShape(), first passing in no parameters and then passing in two unsigned short integers.

The compiler decides which method to call based on the number and type of parameters entered. You can imagine a third overloaded function named DrawShape() that takes one dimension and an enumeration for whether it is the width or height, at the user’s choice.

Using Default Values

Just as global functions can have one or more default values, so can each member function of a class. The same rules apply for declaring the default values, as illustrated in Listing 10.2.

Listing 10.2. Using Default Values

Image

Image

Image


DrawShape(0,0,true)...
******************************
******************************
******************************
******************************
******************************
DrawShape(40,2)...
************************************************************
************************************************************

Image

Listing 10.2 replaces the overloaded DrawShape() function with a single function with default parameters. The function is declared on line 13 to take three parameters. The first two, aWidth and aHeight, are integers, and the third, UseCurrentVals, is a bool that defaults to false.

The implementation for this somewhat awkward function begins on line 29. Remember that whitespace doesn’t matter in C++, so the function header is actually on lines 29–33.

Within the method, the third parameter, UseCurrentValue, is evaluated on line 38. If it is true, the member variables itsWidth and itsHeight are used to set the local variables printWidth and printHeight, respectively.

If UseCurrentValue is false, either because it has defaulted false or was set by the user, the first two parameters are used for setting printWidth and printHeight.

Note that if UseCurrentValue is true, the values of the other two parameters are completely ignored.

Choosing Between Default Values and Overloaded Functions

Listings 10.1 and 10.2 accomplish the same thing, but the overloaded functions in Listing 10.1 are easier to understand and more natural to use. Also, if a third variation is needed—perhaps the user wants to supply either the width or the height, but not both—it is easy to extend the overloaded functions. The default value, however, will quickly become unusably complex as new variations are added.

How do you decide whether to use function overloading or default values? Here’s a rule of thumb:

Use function overloading when

• No reasonable default value exists.

• You need different algorithms.

• You need to support different types in your parameter list.

The Default Constructor

The point of a constructor is to establish the object; for example, the point of a Rectangle constructor is to make a valid rectangle object. Before the constructor runs, no rectangle exists, only an area of memory. After the constructor finishes, there is a complete, ready-to-use rectangle object. This is a key benefit of object-oriented programming—the calling program does not have to do anything to ensure that the object starts in a self-consistent state.

As discussed on Day 6, “Understanding Object-Oriented Programming,” if you do not explicitly declare a constructor for your class, a default constructor is created that takes no parameters and does nothing. You are free to make your own default constructor, however, that takes no arguments but that “sets up” your object as required.

The constructor provided for you is called the “default” constructor, but by convention so is any constructor that takes no parameters. This can be a bit confusing, but it is usually clear which one is meant from the context.

Take note that if you make any constructors at all, the default constructor is not provided by the compiler. So if you want a constructor that takes no parameters and you’ve created any other constructors, you must add the default constructor yourself!

Overloading Constructors

Constructors, like all member functions, can be overloaded. The capability to overload constructors is very powerful and very flexible.

For example, you might have a rectangle object that has two constructors: The first takes a length and a width and makes a rectangle of that size. The second takes no values and makes a default-sized rectangle. Listing 10.3 implements this idea.

Listing 10.3. Overloading the Constructor


1:   // Listing 10.3 - Overloading constructors
2:
3:   #include <iostream>
4:   using namespace std;
5:
6:   class Rectangle
7:   {
8:     public:
9:       Rectangle();
10:       Rectangle(int width, int length);
11:       ~Rectangle() {}
12:       int GetWidth() const { return itsWidth; }
13:       int GetLength() const { return itsLength; }
14:     private:
15:       int itsWidth;
16:       int itsLength;
17:   };
18:
19:   Rectangle::Rectangle()
20:   {

21:      itsWidth = 5;
22:      itsLength = 10;
23:   }
24:
25:   Rectangle::Rectangle (int width, int length)
26:   {
27:      itsWidth = width;
28:      itsLength = length;
29:   }
30:
31:   int main()
32:   {
33:      Rectangle Rect1;
34:      cout << "Rect1 width: " << Rect1.GetWidth() << endl;
35:      cout << "Rect1 length: " << Rect1.GetLength() << endl;
36:
37:      int aWidth, aLength;
38:      cout << "Enter a width: ";
39:      cin >> aWidth;
40:      cout << " Enter a length: ";
41:      cin >> aLength;
42:
43:      Rectangle Rect2(aWidth, aLength);
44:      cout << " Rect2 width: " << Rect2.GetWidth() << endl;
45:      cout << "Rect2 length: " << Rect2.GetLength() << endl;
46:      return 0;
47:   }

Image


Rect1 width: 5
Rect1 length: 10
Enter a width: 20

Enter a length: 50

Rect2 width: 20
Rect2 length: 50

Image

The Rectangle class is declared on lines 6–17. Two constructors are declared: the “default constructor” on line 9 and a second constructor on line 10, which takes two integer variables.

On line 33, a rectangle is created using the default constructor, and its values are printed on lines 34 and 35. On lines 38–41, the user is prompted for a width and length, and the constructor taking two parameters is called on line 43. Finally, the width and height for this rectangle are printed on lines 44 and 45.

Just as it does any overloaded function, the compiler chooses the right constructor, based on the number and type of the parameters.

Initializing Objects

Up to now, you’ve been setting the member variables of objects in the body of the constructor. Constructors, however, are invoked in two stages: the initialization stage and the body.

Most variables can be set in either stage, either by initializing in the initialization stage or by assigning in the body of the constructor. It is cleaner, and often more efficient, to initialize member variables at the initialization stage. The following example shows how to initialize member variables:

Image

After the closing parentheses on the constructor’s parameter list, write a colon. Then write the name of the member variable and a pair of parentheses. Inside the parentheses, write the expression to be used to initialize that member variable. If more than one initialization exists, separate each one with a comma.

Listing 10.4 shows the definition of the constructors from Listing 10.3 with initialization of the member variables in the initialization portion of the constructor rather than by doing assignments in the body.

Listing 10.4. A Code Snippet Showing Initialization of Member Variables


1:   // Listing 10.4 – Initializing Member Variables
2:   Rectangle::Rectangle():
3:            itsWidth(5),
4:            itsLength(10)
5:   {
6:   }
7:
8:   Rectangle::Rectangle (int width, int length):
9:            itsWidth(width),
10:          itsLength(length)
11:   {
12:   }

Image

Listing 10.4 is just a snippet of code, so there isn’t output. Looking at the code, line 2 starts the default constructor. As was mentioned previously, after the standard header, a colon was added. This is followed by setting default values of 5 and 10 for itsWidth and itsLength on lines 3 and 4.

Line 8 contains the second constructor definition. In this overloaded version, two parameters are passed. These parameters are then set to the class’s members on lines 9 and 10.

Some variables must be initialized and cannot be assigned to, such as references and constants. It is common to have other assignments or action statements in the body of the constructor; however, it is best to use initialization as much as possible.

The Copy Constructor

In addition to providing a default constructor and destructor, the compiler provides a default copy constructor. The copy constructor is called every time a copy of an object is made.

As you learned on Day 9, “Exploiting References,” when you pass an object by value, either into a function or as a function’s return value, a temporary copy of that object is made. If the object is a user-defined object, the class’s copy constructor is called. You saw this yesterday in Listing 9.6.

All copy constructors take one parameter, a reference to an object of the same class. It is a good idea to make it a constant reference because the constructor will not have to alter the object passed in. For example,


Cat(const Cat & theCat);

Here, the Cat constructor takes a constant reference to an existing Cat object. The goal of this copy constructor is to make a copy of theCat.

The default copy constructor simply copies each member variable from the object passed as a parameter to the member variables of the new object. This is called a member-wise (or shallow) copy, and although this is fine for most member variables, it breaks pretty quickly for member variables that are pointers to objects on the free store.

A shallow or member-wise copy copies the exact values of one object’s member variables into another object. Pointers in both objects end up pointing to the same memory. A deep copy copies the values allocated on the heap to newly allocated memory.

If the Cat class includes a member variable, itsAge, that is a pointer to an integer on the free store, the default copy constructor copies the passed-in Cat’s itsAge member variable to the new Cat’s itsAge member variable. The two objects now point to the same memory, as illustrated in Figure 10.1.

Figure 10.1. Using the default copy constructor.

Image

This leads to a disaster when either Cat goes out of scope. If the original Cat’s destructor frees this memory and the new Cat is still pointing to the memory, a stray pointer has been created, and the program is in mortal danger. Figure 10.2 illustrates this problem.

Figure 10.2. Creating a stray pointer.

Image

The solution to this is to create your own copy constructor and to allocate the memory as required. After the memory is allocated, the old values can be copied into the new memory. This is called a deep copy. Listing 10.5 illustrates how to do this.

Listing 10.5. Copy Constructors

Image

Image

Image


Frisky’s age: 5
Setting Frisky to 6...
Creating Boots from Frisky
Frisky’s age: 6
Boots’ age:  6
setting Frisky to 7...
Frisky’s age: 7
Boots’ age: 6

Image

On lines 6–19, the Cat class is declared. Note that on line 9 a default constructor is declared, and on line 10 a copy constructor is declared. You know this is a copy constructor on line 10 because the constructor is receiving a reference—a constant reference in this case—to an object of its same type.

On lines 17 and 18, two member variables are declared, each as a pointer to an integer. Typically, there is little reason for a class to store int member variables as pointers, but this was done to illustrate how to manage member variables on the free store.

The default constructor on lines 21–27 allocates room on the free store for two int variables and then assigns values to them.

The copy constructor begins on line 29. Note that the parameter is rhs. It is common to refer to the parameter to a copy constructor as rhs, which stands for right-hand side. When you look at the assignments on lines 33 and 34, you’ll see that the object passed in as a parameter is on the right-hand side of the equal sign. Here’s how it works:

On lines 31 and 32, memory is allocated on the free store. Then, on lines 33 and 34, the value at the new memory location is assigned the values from the existing Cat.

The parameter rhs is a Cat object that is passed into the copy constructor as a constant reference. As a Cat object, rhs has all the member variables of any other Cat.

Any Cat object can access private member variables of any other Cat object; however, it is good programming practice to use public accessor methods when possible. The member function rhs.GetAge() returns the value stored in the memory pointed to by rhs’s member variable itsAge. In a real-world application, you should get the value for itsWeight in the same way—using an accessor method. On line 34, however, you see confirmation that different objects of the same class can access each other’s members. In this case, a copy is made directly from the rhs object’s private itsWeight member.

Figure 10.3 diagrams what is happening here. The values pointed to by the existing Cat’s member variables are copied to the memory allocated for the new Cat.

Figure 10.3. Deep copy illustrated.

Image

On line 47, a Cat called Frisky is created. Frisky’s age is printed, and then his age is set to 6 on line 50. On line 52, a new Cat, Boots, is created, using the copy constructor and passing in Frisky. Had Frisky been passed as a parameter to a function by value (not by reference), this same call to the copy constructor would have been made by the compiler.

On lines 53 and 54, the ages of both Cats are printed. Sure enough, Boots has Frisky’s age, 6, not the default age of 5. On line 56, Frisky’s age is set to 7, and then the ages are printed again. This time Frisky’s age is 7, but Boots’s age is still 6, demonstrating that they are stored in separate areas of memory.

When the Cats fall out of scope, their destructors are automatically invoked. The implementation of the Cat destructor is shown on lines 37–43. delete is called on both pointers, itsAge and itsWeight, returning the allocated memory to the free store. Also, for safety, the pointers are reassigned to a null value.

Operator Overloading

C++ has a number of built-in types, including int, float, 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.

To explore operator overloading fully, Listing 10.6 creates a new class, Counter. A Counter object will be used in counting (surprise!) in loops and other applications in which a number must be incremented, decremented, or otherwise tracked.

Listing 10.6. The Counter Class

Image

Image


The value of i is 0

Image

As it stands, this is a pretty useless class. It is defined on lines 6–16. Its only member variable is an int. The default constructor, which is declared on line 9 and whose implementation is on line 18, initializes the one member variable, itsVal, to zero.

Unlike an 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!

Writing an Increment Function

Operator overloading restores much of the functionality that has been stripped out of this class. Two ways exist, for example, to add the capability to increment a Counter object. The first is to write an increment method, as shown in Listing 10.7.

Listing 10.7. Adding an Increment Operator

Image

Image


The value of i is 0
The value of i is 1

Image

Listing 10.7 adds an Increment function, defined on line 13. Although this works, it is cumbersome to use. The program cries out for the capability to add a ++ operator, and, of course, this can be done.

Overloading the Prefix Operator

Prefix operators can be overloaded by declaring functions with the form:


returnType operator op ()

Here, op is the operator to overload. Thus, the ++ operator can be overloaded with the following syntax:


void operator++ ()

This statement indicates that you are overloading the ++ operator and that it will not result in a return value—thus void is the return type. Listing 10.8 demonstrates this alternative.

Listing 10.8. Overloading operator++

Image

Image


The value of i is 0
The value of i is 1
The value of i is 2

Image

On line 15, operator++ is overloaded. You can see on line 15 that the overloaded operator simply increments the value of the private member, itsVal. This overloaded operator is then used on line 31. This use is much closer to the syntax of a built-in type such as int.

At this point, you might consider putting in the extra capabilities for which Counter was created in the first place, such as detecting when the Counter overruns its maximum size. A significant defect exists in the way the increment operator was written, however. If you want to put the Counter on the right side of an assignment, it will fail. For example,


Counter a = ++i;

This code intends to create a new Counter, a, and then assign to it the value in i after i is incremented. The built-in copy constructor will handle the assignment, but the current increment operator does not return a Counter object. It returns void. You can’t assign a void to anything, including a Counter object. (You can’t make something from nothing!)

Returning Types in Overloaded Operator Functions

Clearly, what you want is to return a Counter object so that it can be assigned to another Counter object. Which object should be returned? One approach is to create a temporary object and return that. Listing 10.9 illustrates this approach.

Listing 10.9. Returning a Temporary Object


1:  // Listing 10.9 - operator++ returns a temporary object
2:  
3:  #include <iostream>
4:  
5:  using namespace std;
6:  
7:  class Counter
8:  {
9:    public:
10:      Counter();
11:      ~Counter(){}
12:      int GetItsVal()const { return itsVal; }
13:      void SetItsVal(int x) {itsVal = x; }
14:      void Increment() { ++itsVal; }
15:      Counter operator++ ();
16:  
17:    private:
18:      int itsVal;
19:  
20:  };
21:  
22:  Counter::Counter():
23:  itsVal(0)
24:  {}
25:  
26:  Counter Counter::operator++()
27:  {
28:     ++itsVal;
29:     Counter temp;
30:     temp.SetItsVal(itsVal);
31:     return temp;
32:  }
33:  
34:  int main()
35:  {

36:     Counter i;
37:     cout << "The value of i is " << i.GetItsVal() << endl;
38:     i.Increment();
39:     cout << "The value of i is " << i.GetItsVal() << endl;
40:     ++i;
41:     cout << "The value of i is " << i.GetItsVal() << endl;
42:     Counter a = ++i;
43:     cout << "The value of a: " << a.GetItsVal();
44:     cout << " and i: " << i.GetItsVal() << endl;
45:     return 0;
46:  }

Image


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

Image

In this version, operator++ has been declared on line 15 and is defined on lines 26–32. This version has been declared to return a Counter object. On line 29, a temporary variable, temp, is created, and its value is set to match that in the current object being incremented. When the increment is completed, the temporary variable is returned. You can see on line 42, that the temporary variable that is returned is immediately assigned to a.

Returning Nameless Temporaries

There is really no need to name the temporary object created on line 29. If Counter had a constructor that took a value, you could simply return the result of that constructor as the return value of the increment operator. Listing 10.10 illustrates this idea.

Listing 10.10. Returning a Nameless Temporary Object


1:  // Listing 10.10 - operator++ returns a nameless temporary object
2:  
3:  #include <iostream>
4:  
5:  using namespace std;
6:  
7:  class Counter
8:  {
9:    public:
10:      Counter();
11:      Counter(int val);
12:      ~Counter(){}
13:      int GetItsVal()const { return itsVal; }

14:      void SetItsVal(int x) {itsVal = x; }
15:      void Increment() { ++itsVal; }
16:      Counter operator++ ();
17:  
18:    private:
19:      int itsVal;
20:  };
21:  
22:  Counter::Counter():
23:  itsVal(0)
24:  {}
25:  
26:  Counter::Counter(int val):
27:  itsVal(val)
28:  {}
29:  
30:  Counter Counter::operator++()
31:  {
32:     ++itsVal;
33:     return Counter (itsVal);
34:  }
35:  
36:  int main()
37:  {
38:     Counter i;
39:     cout << "The value of i is " << i.GetItsVal() << endl;
40:     i.Increment();
41:     cout << "The value of i is " << i.GetItsVal() << endl;
42:     ++i;
43:     cout << "The value of i is " << i.GetItsVal() << endl;
44:     Counter a = ++i;
45:     cout << "The value of a: " << a.GetItsVal();
46:     cout << " and i: " << i.GetItsVal() << endl;
47:     return 0;
48:  }

Image


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

Image

On line 11, a new constructor is declared that takes an int. The implementation is on lines 26–28. It initializes itsVal with the passed-in value.

The implementation of operator++ is now simplified. On line 32, itsVal is incremented. Then on line 33, a temporary Counter object is created, initialized to the value in itsVal, and then returned as the result of the operator++.

This is more elegant, but raises the question, “Why create a temporary object at all?” Remember that each temporary object must be constructed and later destroyed—each of these is potentially an expensive operation. Also, the object i already exists and already has the right value, so why not return it? This problem can be solved by using the this pointer.

Using the this Pointer

The this pointer is passed to all member functions, including overloaded operators such as operator++(). In the listings you’ve been creating, this points to i, and if it is dereferenced it returns the object i, which already has the right value in its member variable itsVal. Listing 10.11 illustrates returning the dereferenced this pointer and avoiding the creation of an unneeded temporary object.

Listing 10.11. Returning the this Pointer

Image

Image


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

Image

The implementation of operator++, on lines 25–29, has been changed to dereference the this pointer and to return the current object. This provides a Counter object to be assigned to a. As discussed, 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 the returned Counter.

The returned Counter object must be constant. If it were not, it would be possible to perform operations on that returned object that might change its values. For example, if the returned value were not constant, then you might write line 39 as


Counter a = ++++i;

What you should expect from this is for the increment operator (++) to be called on the result of calling ++i. This would actually result in calling the increment operator on the object, i, twice—which is something you should most likely block.

Try this: Change the return value to nonconstant in both the declaration and the implementation (lines 15 and 25), and change line 39 as shown (++++i). Put a break point in your debugger on line 39 and step in. You will find that you step into the increment operator twice. The increment is being applied to the (now nonconstant) return value.

It is to prevent this that you declare the return value to be constant. If you change lines 15 and 25 back to constant, and leave line 39 as shown (++++i), the compiler complains that you can’t call the increment operator on a constant object.

Overloading the Postfix Operator

So far, you’ve overloaded the prefix operator. 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.

Difference Between Prefix and Postfix

Before you can write the postfix operator, you must understand how it is different from the prefix operator. You learned about this in detail on Day 4, “Creating Expressions and Statements” (see Listing 4.3).

To review, prefix says “increment, and then fetch,” but postfix says “fetch, and then increment.”

Thus, although 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, you must create a temporary object that will hold the original value, increment the value of the original object, and then return the temporary object.

Let’s go over that again. Consider the following line of code:


a = x++;

If x was 5, after this statement a is 5, but x is 6. Thus, the value in x was returned and assigned to a, and then the value of x is increased. 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 original value to a.

Note that because the temporary is being returned, it must be returned by value and not by reference, because the temporary will go out of scope as soon as the function returns.

Listing 10.12 demonstrates the use of both the prefix and the postfix operators.

Listing 10.12. Prefix and Postfix Operators

Image

Image

Image


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: 3 and i: 4

Image

The postfix operator is declared on line 15 and implemented on lines 31–36. The prefix operator is declared on line 14.

The parameter passed into the postfix operator on line 32 (theFlag) serves to signal that it is the postfix operator, but this value is never used.

Overloading Binary Mathematical Operators

The increment operator is a unary operator. It operates on only one object. Many of the mathematical operators are binary operators; they take two objects (one of the current class, and one of any class). Obviously, overloading operators such as the addition (+), subtraction (-), multiplication (*), division (/), and modulus (%) operators is going to be different from overloading the prefix and postfix operators. Consider how you would implement overloading the + operator for Count.

The goal is to be able to declare two Counter variables and then add them, as in the following example:


Counter varOne, varTwo, varThree;
VarThree = VarOne + VarTwo;

Once again, you could start by writing a function, Add(), which would take a Counter as its argument, add the values, and then return a Counter with the result. Listing 10.13 illustrates this approach.

Listing 10.13. The Add() Function

Image

Image


varOne: 2
varTwo: 4
varThree: 6

Image

The Add() function is declared on line 15. It takes a constant Counter reference, which is the number to add to the current object. It returns a Counter object, which is the result to be assigned to the left side of the assignment statement, as shown on line 37. That is, VarOne is the object, varTwo is the parameter to the Add() function, and the result is assigned to VarThree.

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 25–27. Because varOne and varTwo need to be initialized to a nonzero value, another constructor was created, as shown on lines 21–23. Another solution to this problem is to provide the default value 0 to the constructor declared on line 11.

The Add() function itself is shown on lines 29–32. It works, but its use is unnatural.

Overloading the Addition Operator (operator+)

Overloading the + operator would make for a more natural use of the Counter class. Remember, you saw earlier that to overload an operator, you use the structure:


returnType operator op ()

Listing 10.14 illustrates using this to overload the addition operator.

Listing 10.14. operator+


1:  // Listing 10.14 - Overload operator plus (+)
2:  
3:  #include <iostream>
4:  
5:  using namespace std;
6:  
7:  class Counter
8:  {
9:    public:
10:      Counter();
11:      Counter(int initialValue);
12:      ~Counter(){}
13:      int GetItsVal()const { return itsVal; }
14:      void SetItsVal(int x) {itsVal = x; }
15:      Counter operator+ (const Counter &);
16:    private:
17:      int itsVal;
18:  };
19:  
20:  Counter::Counter(int initialValue):
21:  itsVal(initialValue)
22:  {}
23:  
24:  Counter::Counter():
25:  itsVal(0)
26:  {}
27:  
28:  Counter Counter::operator+ (const Counter & rhs)
29:  {
30:     return Counter(itsVal + rhs.GetItsVal());
31:  }
32:  
33:  int main()
34:  {
35:     Counter varOne(2), varTwo(4), varThree;
36:     varThree = varOne + varTwo;
37:     cout << "varOne: " << varOne.GetItsVal()<< endl;
38:     cout << "varTwo: " << varTwo.GetItsVal() << endl;
39:     cout << "varThree: " << varThree.GetItsVal() << endl;
40:  
41:     return 0;
42:  }

Image


varOne: 2
varTwo: 4
varThree: 6

Image

operator+ is declared on line 15 and defined on lines 28–31.

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 to say:


varThree = varOne.Add(varTwo);

Not a big change, but enough to make the program easier to use and understand.

On line 36, the operator is used


36:       varThree = varOne + varTwo;

This is translated by the compiler into


VarThree = varOne.operator+(varTwo);

You could, of course, have written it this way yourself, and the compiler would have been equally happy.

The operator+ method is called on the left-hand operand, passing in the right-hand operand.

Issues in Operator Overloading

Overloaded operators can be member functions, as described in today’s lesson, or nonmember functions. The latter is described on Day 15, “Special Classes and Functions,” when you learn about friend functions.

The only operators that must be class members are the assignment (=), subscript([]), function call (()), and indirection (->) operators.

Operator [] is discussed on Day 13, “Managing Arrays and Strings,” when arrays are covered. Overloading operator -> is discussed on Day 15, when smart pointers are discussed.

Limitations on Operator Overloading

Operators for 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 or binary, cannot be changed. You cannot make up new operators, so you cannot declare ** to be the “power of” operator.

Arity refers to how many terms are used in the operator. Some C++ operators are unary and use only one term (myValue++). Some operators are binary and use two terms (a+b). Only one operator is ternary and uses three terms. The ? operator is often called the ternary operator because it is the only ternary operator in C++ (a > b ? x : y).

What to Overload

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.

Image

The Assignment Operator

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 initialized with itsAge equal to 5 and itsWeight equal to 7. catTwo is then created and assigned the values 3 and 4.

After a while, catTwo is assigned the values in catOne. Two issues are raised here: What happens if itsAge is a pointer, and what happens to the original values in catTwo?

Handling member variables that store their values on the free store was discussed earlier during the examination of the copy constructor. The same issues arise here, as you saw illustrated in Figures 10.1 and 10.2.

C++ programmers differentiate between a shallow, or member-wise, copy on the one hand and a deep copy on the other. A shallow copy just copies the members, and both objects end up pointing to the same area on the free store. A deep copy allocates the necessary memory. This was illustrated in Figure 10.3.

An added wrinkle occurs with the assignment operator, however. The object catTwo already exists and has memory already allocated. That memory must be deleted to avoid any memory leaks. But what happens if you assign catTwo to itself?


catTwo = catTwo;

No one is likely to do this on purpose. It is, however, possible for this to happen by accident when references and dereferenced pointers hide the fact that the assignment is to itself.

If you did not handle this problem carefully, catTwo would delete its memory allocation. Then, when it was ready to copy in the values from memory on the right-hand side of the assignment, there would be a very big problem: The value 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 value of the this pointer. Listing 10.15 shows a class with an assignment operator overloaded. It also avoids the issue just mentioned.

Listing 10.15. An Assignment Operator

Image

Image

Image


Frisky’s age: 5
Setting Frisky to 6...
Whiskers’ age: 5
copying Frisky to Whiskers...
Whiskers’ age: 6

Image

Listing 10.15 brings back the Cat class, but leaves out the copy constructor and destructor to save room. New to the listing on line 15 is the declaration of the assignment operator. This is the method that will be used to overload the assignment operator. On lines 31–38, this overload method is defined.

On line 33, the current object (the Cat being assigned to) is tested to see whether it is the same as the Cat being assigned. This is done by checking whether the address of the Cat object on the right side (rhs) is the same as the address stored in the this pointer. If they are the same, there is no need to do anything because the object on the left is the same object that is on the right. Because of this, line 34 returns the current object.

If the object on the right-hand side is not the same, then the members are copied on lines 35 and 36 before returning.

You see the use of the assignment operator on line 50 of the main program when a Cat object called Frisky is assigned to the Cat object Whiskers. The rest of this listing should be familiar.

This listing assumes that if the two objects are pointing to the same address, then they must be the same. 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.

Handling Data Type Conversion

Now that you’ve seen how to assign an object to another object of the same type, consider another situation. What happens when you try to assign a variable of a built-in type, such as int or unsigned short, to an object of a user-defined class? For example, the Counter class was created earlier. What if you wanted to assign an integer to this class? Listing 10.16 attempts to do this.

Caution

Listing 10.16 will not compile!

Listing 10.16. Attempting to Assign a Counter to an int


1:  // Listing 10.16 - This code won’t compile!
2:  
3:  #include <iostream>
4:  
5:  using namespace std;
6:  
7:  class Counter
8:  {
9:    public:
10:      Counter();
11:      ~Counter(){}
12:      int GetItsVal()const { return itsVal; }
13:      void SetItsVal(int x) {itsVal = x; }
14:    private:
15:      int itsVal;
16:  };
17:  
18:  Counter::Counter():
19:  itsVal(0)
20:  {}
21:  
22:  int main()
23:  {
24:     int theInt = 5;
25:     Counter theCtr = theInt;
26:     cout << "theCtr: " << theCtr.GetItsVal() << endl;
27:     return 0;
28:  }

Image


Compiler error! Unable to convert int to Counter

Image

The Counter class declared on lines 7–16 has only a default constructor. It does not declare methods for turning any built-in types into a Counter object. In the main() function, an integer is declared on line 24. This is then assigned to a Counter object. This line, however, leads to an error. The compiler cannot figure out, unless you tell it that, given an int, it should assign that value to the member variable itsVal.

Listing 10.17 corrects this by creating a conversion operator: a constructor that takes an int and produces a Counter object.

Listing 10.17. Converting int to Counter


1:  // Listing 10.17 - Constructor as conversion operator
2:  
3:  #include <iostream>
4:  
5:  using namespace std;
6:  
7:  class Counter
8:  {
9:    public:
10:      Counter();
11:      Counter(int val);

12:      ~Counter(){}
13:      int GetItsVal()const { return itsVal; }
14:      void SetItsVal(int x) {itsVal = x; }
15:    private:
16:      int itsVal;
17:  };
18:  
19:  Counter::Counter():
20:  itsVal(0)
21:  {}
22:  
23:  Counter::Counter(int val):
24:  itsVal(val)
25:  {}
26:  
27:  int main()
28:  {
29:     int theInt = 5;
30:     Counter theCtr = theInt;
31:     cout << "theCtr: " << theCtr.GetItsVal() << endl;
32:     return 0;
33:  }

Image


theCtr: 5

Image

The important change is on line 11, where the constructor is overloaded to take an int, and on lines 23–25, where the constructor is implemented. The effect of this constructor is to create a Counter out of an int.

Given this, the compiler is able to call the constructor that takes an int as its argument. Here’s how:

Step 1: Create a Counter called theCtr.

This is like saying int x = 5; which creates an integer variable x and then initializes it with the value 5. In this case, a Counter object theCtr is being created and initialized with the integer variable theInt.

Step 2: Assign to theCtr the value of theInt.

But theInt is an integer, not a counter! First, you have to convert it into a Counter. The compiler will try to make certain conversions for you automatically, but you have to teach it how. You teach the compiler how to make the conversion by creating a constructor. In this case, you need a constructor for Counter that takes an integer as its only parameter:


class Counter
{
   Counter (int x);
   // ..
};

This constructor creates Counter objects from integers. It does this by creating a temporary and unnamed counter. For illustration purposes, suppose that the temporary Counter object created from the integer is called wasInt.

Step 3: Assign wasInt to theCtr, which is equivalent to


theCtr = wasInt;

In this step, wasInt (the temporary Counter created when you ran the constructor) is substituted for what was on the right-hand side of the assignment operator. That is, now that the compiler has made a temporary Counter for you, it initializes theCtr with that temporary object.

To further understand this, you must understand that all operator overloading works the same way—you declare an overloaded operator using the keyword operator. With binary operators (such as = or +), the right-hand side variable becomes the parameter. This is done by the constructor. Thus


a = b;

becomes


a.operator=(b);

What happens, however, if you try to reverse the assignment with the following?


1:  Counter theCtr(5);
2:  int theInt = theCtr;
3:  cout << "theInt : " << theInt  << endl;

Again, this generates a compile error. Although the compiler now knows how to create a Counter out of an int, it does not know how to reverse the process.

Conversion Operators

To solve the conversion back to a different type from objects of your class, C++ provides conversion operators. These conversion operators can be added to your class. This enables your class to specify how to do implicit conversions to built-in types. Listing 10.18 illustrates this. One note, however: Conversion operators do not specify a return value, even though they do return a converted value.

Listing 10.18. Converting from Counter to unsigned short()


1:    // Listing 10.18 - Conversion Operators
2:    #include <iostream>
3:    
4:    class Counter
5:    {
6:      public:
7:        Counter();
8:        Counter(int val);
9:        ~Counter(){}
10:        int GetItsVal()const { return itsVal; }
11:        void SetItsVal(int x) {itsVal = x; }
12:        operator unsigned int();
13:      private:
14:        int itsVal;
15:    };
16:    
17:    Counter::Counter():
18:    itsVal(0)
19:    {}
20:    
21:    Counter::Counter(int val):
22:    itsVal(val)
23:    {}
24:    
25:    Counter::operator unsigned int ()
26:    {
27:       return ( int (itsVal) );
28:    }
29:    
30:    int main()
31:    {
32:       Counter ctr(5);
33:       int theInt = ctr;
34:       std::cout << "theInt: " << theInt << std::endl;
35:       return 0;
36:    }

Image


theShort: 5

Image

On line 12, the conversion operator is declared. Note that this declaration starts with the operator keyword, and that it has no return value. The implementation of this function is on lines 25–28. Line 27 returns the value of itsVal, converted to an int.

Now, the compiler knows how to turn ints into Counter objects and vice versa, and they can be assigned to one another freely. You assign and return other data types in the exact same manner.

Summary

Today, you learned how to overload member functions of your classes. You also learned how to supply default values to functions and how to decide when to use default values and when to overload.

Overloading class constructors enables you to create flexible classes that can be created from other objects. Initialization of objects happens at the initialization stage of construction and is more efficient than assigning values in the body of the constructor.

The copy constructor and the assignment operator are supplied by the compiler if you don’t create your own, but they do a member-wise copy of the class. In classes in which member data includes pointers to the free store, these methods must be overridden so that you can allocate memory for the target member variable.

Almost all C++ operators can be overloaded, although you want to be cautious not to create operators whose use is counterintuitive. You cannot change the arity of operators, nor can you invent new operators.

this refers to the current object and is an invisible parameter to all member functions. The dereferenced this pointer is often returned by overloaded operators so that they can participate in expressions.

Conversion operators enable you to create classes that can be used in expressions that expect a different type of object. They are exceptions to the rule that all functions return an explicit value; like constructors and destructors, they have no return type.

Q&A

Q   Why would I ever use default values when I can overload a function?

A   It is easier to maintain one function than two, and it is often easier to understand a function with default parameters than to study the bodies of two functions. Furthermore, updating one of the functions and neglecting to update the second is a common source of bugs.

Q   Given the problems with overloaded functions, why not always use default values instead?

A   Overloaded functions supply capabilities not available with default variables, such as varying the list of parameters by type rather than just by number or providing a different implementation for different parameter type combinations.

Q   When writing a class constructor, how do I decide what to put in the initialization and what to put in the body of the constructor?

A   A simple rule of thumb is to do as much as possible in the initialization phase—that is, initialize all member variables there. Some things, like computations (including those used for initialization) and print statements, must be in the body of the constructor.

Q   Can an overloaded function have a default parameter?

A   Yes. One or more of the overloaded functions can have its own default values, following the normal rules for default variables in any function.

Q   Why are some member functions defined within the class declaration and others are not?

A   Defining the implementation of a member function within the declaration makes it inline. Generally, this is done only if the function is extremely simple. Note that you can also make a member function inline by using the keyword inline, even if the function is declared outside the class declaration.

Workshop

The Workshop provides quiz questions to help solidify your understanding of the material covered and exercises to provide you with experience in using what you’ve learned. Try to answer the quiz and exercise questions before checking the answers in Appendix D, and be certain you understand the answers before going to tomorrow’s lesson.

Quiz

1. When you overload member functions, in what ways must they differ?

2. What is the difference between a declaration and a definition?

3. When is the copy constructor called?

4. When is the destructor called?

5. How does the copy constructor differ from the assignment operator (=)?

6. What is the this pointer?

7. How do you differentiate between overloading the prefix and postfix increment operators?

8. Can you overload the operator+ for short integers?

9. Is it legal in C++ to overload the operator++ so that it decrements a value in your class?

10. What return value must conversion operators have in their declarations?

Exercises

1. Write a SimpleCircle class declaration (only) with one member variable: itsRadius. Include a default constructor, a destructor, and accessor methods for radius.

2. Using the class you created in Exercise 1, write the implementation of the default constructor, initializing itsRadius with the value 5. Do this within the initialization phase of the constructor and not within the body.

3. Using the same class, add a second constructor that takes a value as its parameter and assigns that value to itsRadius.

4. Create a prefix and postfix increment operator for your SimpleCircle class that increments itsRadius.

5. Change SimpleCircle to store itsRadius on the free store, and fix the existing methods.

6. Provide a copy constructor for SimpleCircle.

7. Provide an assignment operator for SimpleCircle.

8. Write a program that creates two SimpleCircle objects. Use the default constructor on one and instantiate the other with the value 9. Call the increment operator on each and then print their values. Finally, assign the second to the first and print its values.

9. BUG BUSTERS: What is wrong with this implementation of the assignment operator?


SQUARE SQUARE ::operator=(const SQUARE & rhs)
{
        itsSide = new int;
        *itsSide = rhs.GetSide();
        return *this;
}

10. BUG BUSTERS: What is wrong with this implementation of the addition operator?


VeryShort  VeryShort::operator+ (const VeryShort& rhs)
{
     itsVal += rhs.GetItsVal();
     return *this;
}

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.33.235