Hour 14. Calling Advanced Functions

Overloaded Member Functions

In Hour 5, “Calling Functions,” you learned how to implement function overloading by writing multiple functions with the same name but different parameters. Member functions also can be overloaded.

This is demonstrated by the Rectangle program in Listing 14.1, which implements a Rectangle class that has two drawShape() functions. One takes no parameters and draws the Rectangle based on the object’s current values. The other takes two values, width and length, and draws a rectangle using those values, ignoring the current values.

Listing 14.1 The Full Text of Rectangle.cpp


 1: #include <iostream>
 2:
 3: class Rectangle
 4: {
 5: public:
 6:     Rectangle(int width, int height);
 7:     ~Rectangle(){}
 8:
 9:     void drawShape() const;
10:     void drawShape(int width, int height) const;
11:
12: private:
13:     int width;
14:     int height;
15: };
16:
17: Rectangle::Rectangle(int newWidth, int newHeight)
18: {
19:     width = newWidth;
20:     height = newHeight;
21: }
22:
23: void Rectangle::drawShape() const
24: {
25:     drawShape(width, height);
26: }
27:
28: void Rectangle::drawShape(int width, int height) const
29: {
30:     for (int i = 0; i < height; i++)
31:     {
32:         for (int j = 0; j < width; j++)
33:         {
34:             std::cout << "*";
35:         }
36:         std::cout << " ";
37:     }
38: }
39:
40: int main()
41: {
42:     Rectangle box(30, 5);
43:     std::cout << "drawShape(): ";
44:     box.drawShape();
45:     std::cout << " drawShape(40, 2): ";
46:     box.drawShape(40, 2);
47:     return 0;
48: }


The Rectangle program displays two rectangles consisting of asterisks:

drawShape():
******************************
******************************
******************************
******************************
******************************
drawShape(40, 2):
****************************************
****************************************

On lines 9–10, the drawShape() function is overloaded. The implementation for these overloaded class functions is on lines 23–38.

The version of drawShape() that takes no parameters works by calling the version that takes two parameters, passing in the current member variables. This avoids duplicating similar code in two overloaded functions. When code is duplicated to perform the same task, a change you make later to one function could be overlooked in the other, introducing errors to your program.

The main function on lines 40–48 creates a Rectangle object and calls drawShape() twice, first with no parameters and then with two integers.

The compiler decides which function to call based on the number and type of parameters entered. A potential third overloaded function named drawShape() could take one dimension and use it for both width and height.

Using Default Values

Just as ordinary functions can have one or more default values, so can each function of a class. The same rules apply for declaring the default values, as illustrated by the Rectangle2 program in Listing 14.2.

Listing 14.2 The Full Text of Rectangle2.cpp


 1: #include <iostream>
 2:
 3: class Rectangle
 4: {
 5: public:
 6:    Rectangle(int width, int height);
 7:     ~Rectangle(){}
 8:     void drawShape(int aWidth, int aHeight,
 9:         bool useCurrentValue = false) const;
10: private:
11:     int width;
12:     int height;
13: };
14:
15: Rectangle::Rectangle(int aWidth, int aHeight)
16: {
17:     width = aWidth;
18:     height = aHeight;
19: }
20: void Rectangle::drawShape(
21:     int aWidth,
22:     int aHeight,
23:     bool useCurrentValue
24: ) const
25: {
26:     int printWidth;
27:     int printHeight;
28:
29:     if (useCurrentValue == true)
30:     {
31:         printWidth = width;
32:         printHeight = height;
33:     }
34:     else
35:     {
36:         printWidth = aWidth;
37:         printHeight = aHeight;
38:     }
39:
40:     for (int i = 0; i < printHeight; i++)
41:     {
42:         for (int j = 0; j < printWidth; j++)
43:         {
44:             std::cout << "*";
45:         }
46:         std::cout << " ";
47:     }
48: }
49:
50: int main()
51: {
52:     Rectangle box(20, 5);
53:     std::cout << "drawShape(0, 0, true)... ";
54:     box.drawShape(0, 0, true);
55:     std::cout <<"drawShape(25, 4)... ";
56:     box.drawShape(25, 4);
57:     return 0;
58: }


This program produces the following output:

drawShape(0, 0, true)...
********************
********************
********************
********************
********************
drawShape(25, 4)...
*************************
*************************
*************************
*************************

Listing 14.2 replaces the overloaded drawShape() function with a single function that has default parameters. The function is declared on lines 8–9 to take three parameters. The first two, aWidth and aHeight, are integers. The third, useCurrentValue, is a bool (true or false) that defaults to false.

The implementation for this function begins on line 20. The third parameter, useCurrentValue, is evaluated. If it is true, the member variables width and height are used to set the local variables printWidth and printHeight.

If useCurrentValue is false, either because it defaulted to false or was set by the user to that value, the first two parameters are used to set printWidth and printHeight.


By the Way

The Rectangle and Rectangle2 programs accomplish the same thing, but the overloaded functions in 14.1 are simpler to understand and more natural to use. Also, if a additional variation are needed—perhaps the user wants to supply either the width or the height but not both—it is easier to extend overloaded functions. The default values approach quickly becomes too complex as new variations are added.


Initializing Objects

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

A rectangle object could have two constructors. One takes a length and width as parameters and makes a rectangle of that size. The second takes no parameters and makes a rectangle of a default size specified by the class. The compiler chooses the right constructor based on the number and type of the parameters.

You can overload constructors, but you can’t overload destructors. Destructors always have the same signature: the name of the class prepended by a tilde (~) and no parameters.

Until now, you’ve been setting the member variables of objects in the body of the constructor.

Constructors are created in two stages: the initialization stage and the body of the constructor. A member variable can be set during the initialization or by assigning it a value in the body of the constructor. The following example shows how to initialize member variables:

Tricycle::Tricycle():
speed(5),
wheelSize(12)
{
    // body of constructor
}

To assign values in a constructor’s initialization, put a colon after the closing parentheses of the constructor’s parameter list. After the colon, list the name of a member variable followed by a pair of parentheses. Inside the parentheses, put an expression that initializes the member variable. If more than one variable is being set in this manner, separate each one with a comma.

The preceding example sets the speed member variable to 5 and the wheelSize variable to 12.


Watch Out!

Because references and constants cannot be assigned values, they must be initialized using this technique.


To understand why it is more efficient to initialize member variables than assign to them values, you must understand the copy constructor.

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.

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.

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:

Tricycle(const Tricycle &trike);

In this statement the Tricycle constructor takes a constant reference to an existing Tricycle object. The goal of the copy constructor is to make a copy of trike.

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 shallow (or member-wise) copy. Though this is fine for most member variables, it does not work for member variables that are pointers to objects on the heap.

A shallow 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, on the other hand, copies the values allocated on the heap to newly allocated memory.

An example illustrates the problem: If the Tricycle class includes a member variable called durability pointing to an integer on the heap, the default copy constructor copies the passed-in Tricycle's durability member variable to the new Tricycle's durability member variable. The two objects then point to the same memory, as illustrated in Figure 14.1.

Figure 14.1 Using the default copy constructor.

image

This leads to a disaster when either Tricycle object goes out of scope. That object’s destructor is called and attempts to clean up the allocated memory.

The other object is still pointing to that memory, however. If it tries to access that memory, the program crashes.

The solution to this problem is to define your own copy constructor and allocate memory properly in the copy. Creating a deep copy allows you to copy the existing values into new memory. The DeepCopy program in Listing 14.3 illustrates how to do this.

Listing 14.3 The Full Text of DeepCopy.cpp


 1: #include <iostream>
 2:
 3: class Tricycle
 4: {
 5: public:
 6:     Tricycle();                     // default constructor
 7:     Tricycle(const Tricycle&);      // copy constructor
 8:     ~Tricycle();                    // destructor
 9:     int getSpeed() const { return *speed; }
10:     void setSpeed(int newSpeed) { *speed = newSpeed; }
11:     void pedal();
12:     void brake();
13:
14: private:
15:     int *speed;
16: };
17:
18: Tricycle::Tricycle()
19: {
20:     speed = new int;
21:     *speed = 5;
22: }
23:
24: Tricycle::Tricycle(const Tricycle& rhs)
25: {
26:     speed = new int;
27:     *speed = rhs.getSpeed();
28: }
29:
30: Tricycle::~Tricycle()
31: {
32:     delete speed;
33:     speed = NULL;
34: }
35:
36: void Tricycle::pedal()
37: {
38:     setSpeed(*speed + 1);
39:     std::cout << " Pedaling " << getSpeed() << " mph ";
40: }
41: void Tricycle::brake()
42: {
43:     setSpeed(*speed - 1);
44:     std::cout << " Pedaling " << getSpeed() << " mph ";
45: }
46:
47: int main()
48: {
49:     std::cout << "Creating trike named wichita ...";
50:     Tricycle wichita;
51:     wichita.pedal();
52:     std::cout << "Creating trike named dallas ... ";
53:     Tricycle dallas(wichita);
54:     std::cout << "wichita's speed: " << wichita.getSpeed() << " ";
55:     std::cout << "dallas's speed: " << dallas.getSpeed() << " ";
56:     std::cout << "setting wichita to 10 ... ";
57:     wichita.setSpeed(10);
58:     std::cout << "wichita's speed: " << wichita.getSpeed() << " ";
59:     std::cout << "dallas's speed: " << dallas.getSpeed() << " ";
60:     return 0;
61: }


This program creates two Tricycle objects and takes them for a ride:

Creating trike named wichita ...
Pedaling 6 mph
Creating trike named dallas ...
wichita's speed: 6
dallas's speed: 6
setting wichita to 10 ...
wichita's speed: 10
dallas's speed: 6

On lines 3–16, the Tricycle class is declared. A default constructor (line 6) and copy constructor (line 7) are declared for the class.

On line 15, the speed member variable is declared as a pointer to an integer. (Typically, there is little reason for a class to store int member variables as pointers, but this helps illustrate how to manage member variables on the heap.)

The default constructor on lines 24–28 allocates room on the heap for an int variable and assigns a value to it.

The copy constructor begins on line 24. The parameter’s name is rhs, which stands for right-hand side and is a common naming convention for the parameter of a copy constructor.

Memory is allocated on the heap (line 26) and the value at the new memory location is assigned the value of the speed variable from the existing Tricycle (line 27).

The parameter rhs is a Tricycle passed into the copy constructor as a constant reference. The member function rhs.getSpeed() returns the value stored in the memory pointed to by rhs’s member variable speed. As a Tricycle object, rhs has all the member variables of any other Tricycle.

Figure 14.2 diagrams what is happening here. The values pointed to by the existing Tricycle are copied to the memory allocated for the new Tricycle.

Figure 14.2 An illustration of deep copying.

image

On line 50, a Tricycle is created called wichita. The trike’s pedal() function is called, which increases the speed by 1 and displays the new speed. On line 53, a new Tricycle is created called dallas using the copy constructor and passing in wichita. Had wichita been passed as a parameter to a function, this same call to the copy constructor would have been made by the compiler.

On lines 54–55, the current speed of both Tricycles is displayed. This verifies that a copy was made because dallas has the same speed as wichita, 6, not the default speed of 5. On line 57, wichita’s speed is set to 10, and the speeds of both objects are displayed again. This time dallas has a speed of 10, while wichita remains 6, demonstrating that they are stored in separate areas of memory.

When the Tricycle objects fall out of scope, their destructors are automatically invoked. The implementation of the Tricycle destructor is shown on lines 30–34. delete is called on the pointer and for safety it is reassigned to NULL.

Summary

During this hour, you learned how to achieve more control over the creation and destruction of objects in C++.

Constructors, like functions, can be overloaded. The number and type of parameter to the constructor enables the compiler to determine which one should be called by users of the class.

A constructor may have default values just like member functions and ordinary functions.

When an object is copied, all member variables are copied by the default copy constructor. This creates problems when member variables are pointers to objects on the heap. Both the original object and the copy point to the same object. When one object goes out of scope and is destroyed, the other still has an active pointer to that object. Any attempt to use that pointer results in a crash of the program.

This problem can be fixed by writing your own copy constructor for a class. The constructor takes one parameter, the original object that will be copied. In the constructor, care can be taken so that the pointer uses new heap memory.

Q&A

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

A. Because it’s easier to maintain one function than two and 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 methods, why not always use default values instead?

A. Overloaded methods supply capabilities not available with default variables, such as varying the list of parameters by type rather than just by number.

Q. When writing a class constructor, how do you 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 and initialize all member variables there. Some things such as computations and std::cout statements must be in the body of the constructor.

Q. Can an overloaded method have a default parameter?

A. Yes. There is no reason not to combine these powerful features. One or more of the overloaded methods can have their own default values, following the normal rules for default variables in any method.

Q. Why is horseradish hot?

A. Horseradish, a root native to Russia and Hungary that belongs to the mustard family, contains a mustard-like oil called isothiocyanate that brings tears to your eyes, pain to your nose, and tastes extremely spicy. This only happens when the cells of the horseradish are crushed. Vinegar stops this reaction and stabilizes the flavor.

Both the fumes and the heat deteriorate rapidly upon exposure to the air, so horseradish freshly diced has the maximum capability to inflict culinary suffering. As it gets older, it loses both taste and bite.

The smell of wasabi, also called Japanese horseradish, is so sharply painful that it has been used in experimental smoke detectors for the deaf.

Workshop

For the past hour, you have worked some more with member functions. You should answer a few questions and complete a couple of exercises to reinforce your knowledge of the topic.

Quiz

1. With overloaded functions, how does the compiler know which version to call?

A. The function’s name

B. The function’s number and type of parameters

C. The function’s return type

2. Can you use overloaded functions with defaults?

A. Yes

B. No

C. I don’t need this kind of pressure; I test poorly.

3. When destroying a pointer in a destructor, what should be assigned to the pointer for safety?

A. NULL

B. 0

C. Either a or b

Answers

1. B. The compiler ignores the name and return type. Only the name, number, and parameter type matter.

2. A. Absolutely, as long as the number and types of parameters remain unique among versions of the functions (because that’s how the compiler figures out which one to call).

3. C. Setting it to NULL or 0 has the same effect, making it a null pointer.

Activities

1. Modify the Rectangle program to create another drawShape() method with two integer parameters that include default values.

2. Modify the DeepCopy program to change dallas’s speed after wichita’s speed has been changed. Does a change to dallas affect wichita? You’ve already seen that changing wichita does not affect dallas.

To see solutions to these activities, visit this book’s website at http://cplusplus.cadenhead.org.

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

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