Day 10
Advanced Functions
On Day 5, "Functions," you learned the fundamentals of working
with functions. Now that you know how pointers and references work, there is
more you can do with functions. Today you 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 class member functions
#include
<iostream.h>
3:
typedef
unsigned short int USHORT;
enum
BOOL { FALSE, TRUE};
6:
//
Rectangle class declaration
class
Rectangle
{
public:
//
constructors
Rectangle(USHORT
width, USHORT height);
~Rectangle(){}
14:
//
overloaded class function DrawShape
void
DrawShape() const;
void
DrawShape(USHORT aWidth, USHORT aHeight) const;
private:
USHORT
itsWidth;
USHORT
itsHeight;
};
23:
//Constructor
implementation
Rectangle::Rectangle(USHORT
width, USHORT height)
{
itsWidth
= width;
itsHeight
= height;
}
30:
31:
//
Overloaded DrawShape - takes no values
//
Draws based on current class member values
void
Rectangle::DrawShape() const
{
DrawShape(
itsWidth, itsHeight);
}
38:
39:
//
overloaded DrawShape - takes two values
//
draws shape based on the parameters
void
Rectangle::DrawShape(USHORT width, USHORT height) const
{
for
(USHORT i = 0; i<height; i++)
46:
|
for (USHORT j = 0; j< width; j++)
|
47:
|
{
|
48:
|
cout
<< "*";
|
49:
|
}
|
50:
|
cout
<< "\n";
|
}
}
//
Driver program to demonstrate overloaded functions
int
main()
{
//
initialize a rectangle to 30,5
Rectangle
theRect(30,5);
cout
<< "DrawShape(): \n";
theRect.DrawShape();
cout
<< "\nDrawShape(40,2): \n";
theRect.DrawShape(40,2);
return
0;
}
NOTE: This listing passes width and height values to several functions. You should note that sometimes width is passed first and at other times height is passed
first.
Output: DrawShape():
******************************
******************************
******************************
******************************
******************************
DrawShape(40,2):
************************************************************
************************************************************
Analysis: 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 16 and 17, where DrawShape() is overloaded. The
implementation for these overloaded class methods is on lines 32-52.
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,
The driver program, on lines 54-64, 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. One 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 non-class 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.
//Listing
10.2 Default values in member functions
#include
<iostream.h>
3:
typedef
unsigned short int USHORT;
enum
BOOL { FALSE, TRUE};
6:
//
Rectangle class declaration
class
Rectangle
{
public:
//
constructors
Rectangle(USHORT
width, USHORT height);
~Rectangle(){}
void
DrawShape(USHORT aWidth, USHORT aHeight, BOOL
UseCurrentVals
= Â FALSE)
const;
15:
private:
USHORT
itsWidth;
USHORT
itsHeight;
};
20:
//Constructor
implementation
Rectangle::Rectangle(USHORT
width, USHORT height):
23: itsWidth(width), //
initializations
itsHeight(height)
25:
|
{}
|
//
empty body
|
26:
|
|
|
27:
|
|
|
28:
|
//
|
default values used for third parameter
|
USHORT
width,
USHORT
height,
BOOL
UseCurrentValue
)
const
{
int
printWidth;
int
printHeight;
37:
if
(UseCurrentValue == TRUE)
{
40:
|
printWidth
= itsWidth;
|
// use current class
|
values
|
|
|
41:
|
printHeight
= itsHeight;
|
|
}
else
{
45:
|
printWidth
= width;
|
// use parameter values
|
46:
|
printHeight
= height;
|
|
47:
|
}
|
|
48:
|
|
|
49:
|
|
|
for
(int i = 0; i<printHeight; i++)
{
52:
|
for (int j = 0; j< printWidth; j++)
|
53:
|
{
|
54:
|
cout
<< "*";
|
55:
|
}
|
56:
|
cout
<< "\n";
|
}
}
//
Driver program to demonstrate overloaded functions
int
main()
{
//
initialize a rectangle to 10,20
Rectangle
theRect(30,5);
cout
<< "DrawShape(0,0,TRUE)...\n";
theRect.DrawShape(0,0,TRUE);
cout
<<"DrawShape(40,2)...\n";
theRect.DrawShape(40,2);
return
0;
}
Output:
DrawShape(0,0,TRUE)...
******************************
******************************
******************************
******************************
DrawShape(40,2)...
************************************************************
************************************************************
Analysis: Listing 10.2 replaces
the overloaded DrawShape() function with a single function with
default parameters. The function is declared on line 14 to take three
parameters. The first two, aWidth and
aHeight, are
USHORTs, and the third,
UseCurrentVals, is a
BOOL (true or false) that
defaults to FALSE.
NOTE: Boolean values are those that evaluate to TRUE or FALSE. C++ considers 0 to be false and all other values to be true.
The implementation for this somewhat awkward function begins on line 29.
The third parameter, UseCurrentValue, is
evaluated. 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
● There is
no reasonable default value.
● You need different algorithms.
The Default Constructor
As discussed on Day 6, "Basic Classes," 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 from context which is
meant.
Take note
that if you make any constructors at all, the default constructor is not made
by the compiler. So if you want a constructor that takes no parameters and
you've created any other constructors, you must make the default constructor
yourself!
Overloading Constructors
The point of a constructor is to
establish the object; for example, the point of a Rectangle
constructor is to make a rectangle. Before the constructor runs, there
is no rectangle, just an area of memory. After the constructor finishes, there
is a complete, ready-to-use rectangle object.
Constructors, like all member functions, can be overloaded. The ability
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.
//
Listing 10.3
//
Overloading constructors
4: #include <iostream.h> 5:
class
Rectangle
{
public:
9:
|
Rectangle();
|
10:
|
Rectangle(int
width, int length);
|
11:
|
~Rectangle()
{}
|
12:
|
int
GetWidth() const { return itsWidth; }
|
13:
|
int GetLength() const { return itsLength; }
|
15:
|
int
itsWidth;
|
16:
|
int itsLength;
|
17:
|
};
|
18:
|
|
Rectangle::Rectangle()
{
21:
|
itsWidth
= 5;
|
22:
|
itsLength = 10;
|
23:
|
}
|
24:
|
|
Rectangle::Rectangle
(int width, int length)
{
27: itsWidth = width; 28: itsLength = length;
29: }
30:
int
main()
{
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
<< "\nEnter a length: ";
|
41:
|
cin
>> aLength;
|
42:
|
|
43:
|
Rectangle
Rect2(aWidth, aLength);
|
44:
|
cout
<< "\nRect2 width: " << Rect2.GetWidth() <<
|
endl;
|
|
45:
|
cout
<< "Rect2 length: " << Rect2.GetLength() <<
|
endl;
|
|
return
0;
}
Output: Rect1 width: 5
Rect1 length: 10
Enter a width: 20
Enter a length: 50
Rect2 width: 20
Rect2 length: 50
Analysis: The Rectangle class is declared on lines 6-17. Two constructors are declared:
the "default
constructor" on line 9 and a constructor taking two integer variables.
On line 33, a rectangle is created using the default constructor, and
its values are printed on lines 34-35. On lines 37-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-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:
CAT():
|
//
|
constructor name and parameters
|
itsAge(5),
|
//
|
initialization
list
|
itsWeight(8)
|
|
|
{ }
|
|
//
body of constructor
|
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 there is more than one initialization, 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 rather than
assignment.
Listing 10.4. A code snippet showing initialization of
member variables.
Rectangle::Rectangle():
itsWidth(5),
itsLength(10)
{
};
6:
Rectangle::Rectangle
(int width, int length)
itsWidth(width),
itsLength(length)
10:
11: };
There are some variables that must be initialized
and cannot be assigned to: 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.
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, as you saw
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 the 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.
New Term: 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
points to an integer on the free store, the default copy constructor will copy
the passed-in CAT's itsAge member variable to the new CAT's itsAge member variable. The two objects will now point to the same
memory, as illustrated in Figure 10.1.
This will lead to a disaster when
either CAT goes out of scope. As mentioned on Day 8,
"Pointers," the
job of the destructor is to clean up this memory. 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.
The solution to this is to create your own copy
constructor and to allocate the memory as required. Once 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.
//
Listing 10.5
//
Copy constructors
4: #include <iostream.h> 5:
class
CAT
{
public:
9:
|
CAT();
|
// default constructor
|
10:
|
CAT
(const CAT &);
|
//
copy constructor
|
11:
|
~CAT();
|
//
destructor
|
12:
|
int
GetAge() const { return *itsAge; }
|
|
13:
|
int
GetWeight() const { return *itsWeight; }
|
|
14:
|
void
SetAge(int age) { *itsAge = age; }
|
|
15:
|
|
|
private:
17:
|
int
*itsAge;
|
18:
|
int *itsWeight;
|
19:
|
};
|
20:
|
|
CAT::CAT()
{
itsAge
= new int;
itsWeight
= new int;
*itsAge
= 5;
*itsWeight
= 9;
}
28:
CAT::CAT(const
CAT & rhs)
{
itsAge
= new int;
itsWeight
= new int;
*itsAge
= rhs.GetAge();
*itsWeight
= rhs.GetWeight();
CAT::~CAT()
{
delete
itsAge;
itsAge
= 0;
delete
itsWeight;
itsWeight
= 0;
}
44:
int
main()
{
CAT
frisky;
cout
<< "frisky's age: " << frisky.GetAge() << endl;
cout
<< "Setting frisky to 6...\n";
frisky.SetAge(6);
cout
<< "Creating boots from frisky\n";
CAT
boots(frisky);
53: cout
<< "frisky's age: " << frisky.GetAge()
<< endl;
cout
<< "boots' age: " << boots.GetAge() << endl;
cout
<< "setting frisky to 7...\n";
frisky.SetAge(7);
57: cout
<< "frisky's age: " << frisky.GetAge()
<< endl;
cout
<< "boot's age: " << boots.GetAge() << endl;
return
0;
}
Output: 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
Analysis: 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.
On lines 17 and 18, two member variables are
declared, each as a pointer to an integer. Typically there'd be 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 in
lines 33 and 34, you'll see that the object passed in as a parameter is on the
right-hand side of the equals 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 that is passed into the copy constructor as a constant reference. The
member function rhs.GetAge() returns
the value stored in the memory pointed to by rhs's member variable itsAge. As a CAT object, rhs has all the member variables of
any other CAT.
Operator Overloading
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 10.6 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.
//
Listing 10.6
//
The Counter class
typedef
unsigned short USHORT;
#include
<iostream.h>
6:
class
Counter
{
public:
10:
|
Counter();
|
11:
|
~Counter(){}
|
12:
|
USHORT GetItsVal()const { return itsVal; }
|
13:
|
void
SetItsVal(USHORT x) {itsVal = x; }
|
14:
|
|
private:
16: USHORT itsVal; 17:
18: };
19:
Counter::Counter():
itsVal(0)
{};
23:
int
main()
{
Counter
i;
cout
<< "The value of i is " << i.GetItsVal() << endl;
return
0;
}
Output: The value of i
is 0
Analysis: As it stands, this is a pretty useless class. It is defined
on lines 7-18. Its only member variable is a USHORT. The default
constructor, which is declared on line 10 and whose
implementation is on line 20,
initializes the one member variable, itsVal, to
zero.
Unlike an honest, red-blooded USHORT, 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. For example, there are two ways to add the ability to increment a Counter object. The first is to write an
Listing 10.7. Adding an increment operator.
//
Listing 10.7
//
The Counter class
typedef
unsigned short USHORT;
#include
<iostream.h>
6:
class
Counter
{
public:
10:
|
Counter();
|
11:
|
~Counter(){}
|
12:
|
USHORT GetItsVal()const { return itsVal; }
|
13:
|
void
SetItsVal(USHORT x) {itsVal = x; }
|
14:
|
void
Increment() { ++itsVal; }
|
15:
|
|
private:
17: USHORT
itsVal;
18:
19: };
20:
Counter::Counter():
itsVal(0)
{};
24:
int
main()
{
Counter
i;
cout
<< "The value of i is " << i.GetItsVal() << endl;
i.Increment();
cout
<< "The value of i is " << i.GetItsVal() << endl;
return
0;
}
Output: The value of i
is 0
The value of i is 1
Analysis: Listing 10.7 adds an Increment function, defined on line 14. Although this works, it is cumbersome to use. The program cries out for the ability to
add a ++ operator, and of course this can be done.
Overloading the Prefix Operator
returnType Operator op
(parameters)
Here, op is the operator to overload. Thus, the ++ operator can be overloaded with the following syntax:
void operator++ ()
Listing 10.8 demonstrates this
alternative.
Listing 10.8. Overloading operator++.
//
Listing 10.8
//
The Counter class
typedef
unsigned short USHORT;
#include
<iostream.h>
6:
class
Counter
{
public:
10:
|
Counter();
|
11:
|
~Counter(){}
|
12:
|
USHORT GetItsVal()const { return itsVal; }
|
13:
|
void
SetItsVal(USHORT x) {itsVal = x; }
|
14:
|
void
Increment() { ++itsVal; }
|
15:
|
void
operator++ () { ++itsVal; }
|
16:
|
|
private:
18: USHORT
itsVal;
19:
20: };
21:
Counter::Counter():
itsVal(0)
{};
25:
int
main()
{
Counter
i;
cout
<< "The value of i is " << i.GetItsVal() << endl;
i.Increment();
cout
<< "The value of i is " << i.GetItsVal() << endl;
cout
<< "The value of i is " << i.GetItsVal() << endl;
return
0;
}
Output: The value of i
is 0
The value of i is 1
The value of i is 2
Analysis: On line 15, operator++ is overloaded, and it's used on line 32. This is far closer to the syntax one would expect with the Counter object. At this
point, you might consider putting in the extra abilities for which Counter was created in the
first place, such as detecting when the Counter overruns its maximum size.
There is a significant defect 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 object
to 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 would be to
create a temporary object and return that. Listing 10.9 illustrates this
approach.
Listing 10.9. Returning a temporary object.
//
Listing 10.9
//
operator++ returns a temporary object
typedef
unsigned short USHORT;
#include
<iostream.h>
6:
class
Counter
{
public:
10:
|
Counter();
|
11:
|
~Counter(){}
|
12:
|
USHORT GetItsVal()const { return itsVal; }
|
13:
|
void
SetItsVal(USHORT x) {itsVal = x; }
|
14:
|
void
Increment() { ++itsVal; }
|
15:
|
Counter
operator++ ();
|
private:
18: USHORT itsVal; 19:
20: };
21:
Counter::Counter():
itsVal(0)
{};
25:
Counter
Counter::operator++()
{
++itsVal;
Counter
temp;
temp.SetItsVal(itsVal);
return
temp;
}
33:
int
main()
{
Counter
i;
cout
<< "The value of i is " << i.GetItsVal() << endl;
i.Increment();
cout
<< "The value of i is " << i.GetItsVal() << endl;
++i;
cout
<< "The value of i is " << i.GetItsVal() << endl;
Counter
a = ++i;
cout
<< "The value of a: " << a.GetItsVal();
cout
<< " and i: " << i.GetItsVal() << endl;
return
0;
}
Output: 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
Analysis: In this version, operator++ has been declared on line 15 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. That temporary variable is
returned and immediately assigned to a on line 42.
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
//
operator++ returns a nameless temporary object
typedef
unsigned short USHORT;
#include
<iostream.h>
6:
class
Counter
{
public:
10:
|
Counter();
|
11:
|
Counter(USHORT
val);
|
12:
|
~Counter(){}
|
13:
|
USHORT GetItsVal()const { return itsVal; }
|
14:
|
void
SetItsVal(USHORT x) {itsVal = x; }
|
15:
|
void
Increment() { ++itsVal; }
|
16:
|
Counter
operator++ ();
|
17:
|
|
private:
19: USHORT itsVal; 20:
21: };
22:
Counter::Counter():
itsVal(0)
{}
26:
Counter::Counter(USHORT
val):
itsVal(val)
{}
30:
Counter
Counter::operator++()
{
++itsVal;
return
Counter (itsVal);
}
36:
int
main()
{
Counter
i;
cout
<< "The value of i is " << i.GetItsVal() << endl;
i.Increment();
cout
<< "The value of i is " << i.GetItsVal() << endl;
++i;
Counter
a = ++i;
cout
<< "The value of a: " << a.GetItsVal();
cout
<< " and i: " << i.GetItsVal() << endl;
return
0;
}
Output: 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
Analysis: On line 11, a new constructor is declared that takes a USHORT. The implementation
is on lines 27-29. It
initializes itsVal with the passed-in value.
The implementation of operator++ is now
simplified. On line 33, itsVal is
incremented. Then on line 34, 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 begs the question, "Why create a
temporary object at all?" Remember that each temporary object must be
constructed and later destroyed--each one potentially an expensive operation.
Also, the object i already exists and already has
the right value, so why not return it? We'll solve this problem by using the this pointer.
Using the this Pointer
The this pointer,
as discussed yesterday, was passed to the operator++ member
function as to all
member functions. The this pointer
points to i, and if it's dereferenced it
will return 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.
//
Listing 10.11
//
Returning the dereferenced this pointer
typedef
unsigned short USHORT;
#include
<iostream.h>
6:
class
Counter
{
public:
10:
|
Counter();
|
11:
|
~Counter(){}
|
12:
|
USHORT GetItsVal()const { return itsVal; }
|
13:
|
void
SetItsVal(USHORT x) {itsVal = x; }
|
15: const Counter& operator++ (); 16:
private:
18: USHORT
itsVal;
19:
20: }; 21:
Counter::Counter():
itsVal(0)
{};
25:
const
Counter& Counter::operator++()
{
++itsVal;
return
*this;
}
31:
int
main()
{
Counter
i;
cout
<< "The value of i is " << i.GetItsVal() << endl;
i.Increment();
cout
<< "The value of i is " << i.GetItsVal() << endl;
++i;
cout
<< "The value of i is " << i.GetItsVal() << endl;
Counter
a = ++i;
cout
<< "The value of a: " << a.GetItsVal();
cout
<< " and i: " << i.GetItsVal() << endl;
return
0;
}
Output: 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
Analysis: The implementation of operator++, on lines 26-30, 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 above, 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.
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 we can write the postfix operator, we must understand how it is
different from the prefix operator. We reviewed this in detail on Day 4,
"Expressions and Statements" (see Listing 4.3).
To review, prefix says
"increment, and then fetch," while postfix says "fetch, and then
increment."
Thus, 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, we must create a temporary object that will
hold the original value, then increment the value of the original object, and
then return the temporary.
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, we returned the value in x and
assigned it to a, and then we increased 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
to assign its value to a.
Note that
since we are returning the temporary, we must return it by value and not by
reference, as 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.
//
Listing 10.12
//
Returning the dereferenced this pointer
typedef
unsigned short USHORT;
#include
<iostream.h>
6:
class
Counter
{
public:
~Counter(){}
USHORT
GetItsVal()const { return itsVal; }
void
SetItsVal(USHORT x) {itsVal = x; }
14:
|
const
Counter& operator++ ();
|
// prefix
|
15:
|
const
Counter operator++ (int); // postfix
|
|
16:
|
|
|
private:
USHORT
itsVal;
};
20:
Counter::Counter():
itsVal(0)
{}
24:
const
Counter& Counter::operator++()
{
++itsVal;
return
*this;
}
30:
const
Counter Counter::operator++(int)
{
Counter
temp(*this);
++itsVal;
return
temp;
}
37:
int
main()
{
Counter
i;
cout
<< "The value of i is " << i.GetItsVal() << endl;
i++;
cout
<< "The value of i is " << i.GetItsVal() << endl;
++i;
cout
<< "The value of i is " << i.GetItsVal() << endl;
Counter
a = ++i;
cout
<< "The value of a: " << a.GetItsVal();
cout
<< " and i: " << i.GetItsVal() << endl;
a
= i++;
cout
<< "The value of a: " << a.GetItsVal();
cout
<< " and i: " << i.GetItsVal() << endl;
return
0;
}
Output: The value of i
is 0
The value of i is 2
The value of a: 3 and
i: 3
The value of a: 3 and
i: 4
Analysis: The postfix operator is declared on line 15 and implemented
on lines 31-36. Note that the call to the prefix
operator on line 14 does not include the flag integer (x), but is used with
its normal syntax. The postfix operator uses a flag value (x) to signal that it
is the postfix and not the prefix. The flag value (x) is never used, however.
Operator Overloading Unary Operators
Declare an overloaded operator as you would a
function. Use the keyword operator,
followed by the operator to overload. Unary operator functions do not take
parameters, with the exception of the postfix increment and decrement, which
take an integer as a flag. Example 1
const Counter&
Counter::operator++ ();
Example 2
Counter
Counter::operator-(int);
DO use a
parameter to operator++ if you want the postfix operator.
DO return a const
reference to the object from
operator++. DON'T
create temporary objects
as return values from operator++.
The Addition Operator
The increment operator is a unary
operator. It operates on only one object. The addition operator (+) is
a binary operator, where two objects are involved. How do you implement
overloading the + operator for Count?
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(), 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.
//
Add function
typedef
unsigned short USHORT;
#include
<iostream.h>
6:
class
Counter
{
public:
Counter();
Counter(USHORT
initialValue);
~Counter(){}
USHORT
GetItsVal()const { return itsVal; }
void
SetItsVal(USHORT x) {itsVal = x; }
Counter
Add(const Counter &);
16:
private:
USHORT
itsVal;
20: };
21:
Counter::Counter(USHORT
initialValue):
itsVal(initialValue)
{}
25:
Counter::Counter():
itsVal(0)
{}
29:
Counter
Counter::Add(const Counter & rhs)
{
return
Counter(itsVal+ rhs.GetItsVal());
}
34:
int
main()
{
Counter
varOne(2), varTwo(4), varThree;
varThree
= varOne.Add(varTwo);
cout
<< "varOne: " << varOne.GetItsVal()<< endl;
cout
<< "varTwo: " << varTwo.GetItsVal() << endl;
cout
<< "varThree: " << varThree.GetItsVal() << endl;
return
0;
}
Output: varOne: 2
Analysis: 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 38. That is, VarOne is the object, varTwo is the parameter to the Add() function, and the result is assigned to VarThree.
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 26-28. Since varOne and varTwo need to be initialized to a non-zero value, another constructor was
created, as shown on
lines 22-24. Another solution to this problem is to provide the default
value 0 to the constructor declared on
line 11.
Overloading operator+
The Add() function itself is shown on lines 30-33. It works, but its use is
unnatural. Overloading the + operator would make for a more
natural use of the Counter class.
Listing 10.14 illustrates this.
Listing 10.14. operator+.
//
Listing 10.14
//Overload
operator plus (+)
typedef
unsigned short USHORT;
#include
<iostream.h>
6:
class
Counter
{
public:
Counter();
Counter(USHORT
initialValue);
~Counter(){}
USHORT
GetItsVal()const { return itsVal; }
void
SetItsVal(USHORT x) {itsVal = x; }
Counter
operator+ (const Counter &);
private:
USHORT
itsVal;
};
19:
Counter::Counter(USHORT
initialValue):
itsVal(initialValue)
{}
23:
itsVal(0)
{}
27:
Counter
Counter::operator+ (const Counter & rhs)
{
return
Counter(itsVal + rhs.GetItsVal());
}
32:
int
main()
{
Counter
varOne(2), varTwo(4), varThree;
varThree
= varOne + varTwo;
cout
<< "varOne: " << varOne.GetItsVal()<< endl;
cout
<< "varTwo: " << varTwo.GetItsVal() << endl;
cout
<< "varThree: " << varThree.GetItsVal() << endl;
return
0;
}
Output: varOne: 2 varTwo: 4 varThree: 6
Analysis: 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.
NOTE: The techniques used for overloading operator++ can be applied to the
other unary operators, such
as operator-.
Operator Overloading: Binary
Operators
Binary operators are created like unary operators, except that they do
take a parameter. The parameter is a constant reference to an object of the
same type. Example 1
Example 2
Counter
Counter::operator-(const Counter & rhs);
Issues in Operator Overloading
Overloaded operators can be
member functions, as described in this chapter, or non-member
functions. The latter will be described on Day 14, "Special Classes
and Functions," when we discuss friend functions.
The only operators that must be class members are the assignment (=), subscript([]), function call (()), and indirection (->)
operators.
operator[]
will be discussed tomorrow, when arrays are
covered. Overloading operator-> will be
discussed on Day 14, when smart pointers are discussed.
Limitations on Operator Overloading
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 or binary, cannot be
changed. You cannot make up new operators, so you cannot declare ** to be the "power of" operator.
New Term: 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, as 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.
DO use operator overloading when it will clarify the program. DON'T create counter-intuitive
operators. DO return an object of
the class from overloaded operators.
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 is illustrated in Figure 10.3.
There is an added wrinkle with
the assignment operator, however. The object catTwo already
exists
and has
memory already allocated. That memory must be deleted if there is to be no
memory leak. But what happens if you assign catTwo to itself?
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 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 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 10.15 shows a class with an assignment operator.
Listing 10.15. An assignment operator.
//
Listing 10.15
//
Copy constructors
4: #include
<iostream.h>
5:
class
CAT
{
8:
|
public:
|
|
9:
|
CAT();
|
// default
|
constructor
|
|
|
//
copy constructor and destructor elided!
11:
|
int
|
GetAge()
const { return *itsAge; }
|
12:
|
int
|
GetWeight() const { return *itsWeight; }
|
13:
|
void
SetAge(int age) { *itsAge = age; }
|
|
14:
|
CAT
|
operator=(const
CAT &);
|
15:
|
|
|
16:
|
private:
|
|
17:
|
int
|
*itsAge;
|
18:
|
int
|
*itsWeight;
|
19:
|
};
|
|
20:
|
|
|
CAT::CAT()
{
23: itsAge
= new int;
itsWeight
= new int;
*itsAge
= 5;
*itsWeight
= 9;
}
28:
29:
CAT
CAT::operator=(const CAT & rhs)
{
if
(this == &rhs)
return
*this;
delete
itsAge;
delete
itsWeight;
itsAge
= new int;
itsWeight
= new int;
*itsAge
= rhs.GetAge();
*itsWeight
= rhs.GetWeight();
}
42:
43:
int
main()
{
46:
|
CAT
frisky;
|
47:
|
cout << "frisky's age: " <<
frisky.GetAge() << endl;
|
48:
|
cout
<< "Setting frisky to 6...\n";
|
49:
|
frisky.SetAge(6);
|
50:
|
CAT
whiskers;
|
51:
|
cout
<< "whiskers' age: " << whiskers.GetAge() <<
|
endl;
|
|
52:
|
cout
<< "copying frisky to whiskers...\n";
|
53:
|
whiskers
= frisky;
|
54:
|
cout
<< "whiskers' age: " << whiskers.GetAge() <<
|
endl;
|
|
return
0;
}
frisky's
age: 5
|
|
|
Setting frisky
|
to 6...
|
|
whiskers'
|
age:
|
5
|
copying frisky
|
to
whiskers...
|
|
whiskers'
|
age:
|
6
|
Output: Listing 10.15 brings back the CAT class, and leaves out the copy constructor and destructor to save room. On line 14, the assignment operator is
declared, and on lines 30-41 it is defined.
Analysis: On line 32, 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 or not the
address of rhs is the same as the address stored in the this pointer.
This works fine for single inheritance, but if you are using multiple
inheritance, as discussed on Day 13, "Polymorphism," this test will
fail. An alternative test is to dereference the this pointer and see if the two objects are the same:
if (*this == rhs)
Of course, the equality operator (==) can be
overloaded as well, allowing you to determine for yourself what it means for
your objects to be equal.
Conversion Operators
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? Listing 10.16 brings back the
Counter class, and
attempts to assign a variable of type USHORT to a Counter object.
WARNING: Listing 10.16 will not compile!
Listing 10.16. Attempting to assign a Counter to a USHORT
//
Listing 10.16
//
This code won't compile!
typedef
unsigned short USHORT;
#include
<iostream.h>
6:
class
Counter
{
9:
|
public:
|
10:
|
Counter();
|
11:
|
~Counter(){}
|
12:
|
USHORT GetItsVal()const { return itsVal; }
|
13:
|
void
SetItsVal(USHORT x) {itsVal = x; }
|
14:
|
private:
|
15:
|
USHORT
itsVal;
|
16:
|
|
17:
|
};
|
18:
|
|
Counter::Counter():
itsVal(0)
{}
22:
int
main()
{
USHORT
theShort = 5;
Counter
theCtr = theShort;
cout
<< "theCtr: " << theCtr.GetItsVal() << endl;
return
;0
}
Output: Compiler error!
Unable to convert USHORT to Counter
Analysis: The Counter class declared on lines 7-17 has only a default constructor. It
declares no particular method for
turning a USHORT into a Counter object, and so line 26 causes a compile error. The compiler
cannot figure out, unless you tell it, that, given a USHORT, it should assign
that value to the member variable itsVal.
Listing 10.17 corrects this by
creating a conversion operator: a constructor that takes a USHORT and
Listing 10.17. Converting USHORT to Counter.
//
Listing 10.17
//
Constructor as conversion operator
typedef
unsigned short USHORT;
#include
<iostream.h>
6:
class
Counter
{
9:
|
public:
|
10:
|
Counter();
|
11:
|
Counter(USHORT
val);
|
12:
|
~Counter(){}
|
13:
|
USHORT GetItsVal()const { return itsVal; }
|
14:
|
void
SetItsVal(USHORT x) {itsVal = x; }
|
15:
|
private:
|
16:
|
USHORT
itsVal;
|
17:
|
|
18:
|
};
|
19:
|
|
Counter::Counter():
itsVal(0)
{}
23:
Counter::Counter(USHORT
val):
itsVal(val)
{}
27:
28:
int
main()
{
USHORT
theShort = 5;
Counter
theCtr = theShort;
cout
<< "theCtr: " << theCtr.GetItsVal() << endl;
return
0;
35:
Output: theCtr: 5
Analysis: The important change is on line 11, where the constructor is
overloaded to take a USHORT, and on lines 24-26,
where the constructor is implemented. The effect of this constructor is to
create a
Counter out of a USHORT.
Given this, the compiler is able
to call the constructor that takes a USHORT as its
argument. What
Counter
theCtr(5);
USHORT
theShort = theCtr;
cout
<< "theShort : " << theShort << endl;
Once again, this will generate a compile error. Although the compiler
now knows how to create a Counter out of a
USHORT, it does not know how to reverse
the process.
Conversion Operators
To solve
this and similar problems, C++ provides conversion operators that can be added
to your class. This allows 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, in
effect, return a converted value.
Listing 10.18. Converting from Counter to unsigned
short().
//
Listing 10.18
//
conversion operator
typedef
unsigned short USHORT;
#include
<iostream.h>
6:
class
Counter
{
public:
Counter();
Counter(USHORT
val);
~Counter(){}
USHORT
GetItsVal()const { return itsVal; }
void
SetItsVal(USHORT x) {itsVal = x; }
operator
unsigned short();
private:
USHORT
itsVal;
18: 19: }; 20:
Counter::Counter():
itsVal(0)
{}
24:
Counter::Counter(USHORT
val):
itsVal(val)
{}
Counter::operator
unsigned short ()
{
return
( USHORT (itsVal) );
}
33:
int
main()
{
Counter
ctr(5);
USHORT
theShort = ctr;
cout
<< "theShort: " << theShort << endl;
return
0;
40:
Output: theShort: 5
Analysis: On line 15, the conversion operator is declared. Note that
it has no return value. The implementation of
this function is on lines 29-32. Line 31 returns the value of itsVal, converted to a USHORT.
Now the compiler knows how to turn USHORTs into Counter objects and vice versa, and they can be assigned to one another freely.
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 allows 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 allocate memory for the target object.
Almost all C++ operators can be overloaded, though you want to be
cautious not to create operators whose use is counter-intuitive. You cannot
change the arity of operators, nor can you invent new operators.
The this pointer
refers to the current object and is an invisible parameter to all member
functions. The dereferenced this pointer
is often returned by overloaded operators.
Conversion operators allow 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
Why would
you ever use default values when you can overload a function?
It is easier to maintain one
function than two, and 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.
Given the problems with overloaded functions, why not always use default
values instead?
Overloaded functions supply
capabilities not available with default variables, such as varying the list of
parameters by type rather than just by number.
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 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 and print statements, must be
in the body of the constructor.
Can an
overloaded function have a default parameter?
Yes. There is no reason not to
combine these powerful features. One or more of the overloaded functions can
have their own default values, following the normal rules for default variables
in any function.
Why are some member functions defined within the class declaration and
others are not?
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 make sure you understand the answers
before going to the next chapter.
Quiz
What is the difference between a declaration and a
definition?
When is the copy constructor called?
When is the destructor called?
How does the copy constructor differ from the
assignment operator (=)?
What is the this pointer?
How do you differentiate between overloading the prefix
and postfix increment operators?
Can you overload the operator+ for
short integers?
Is it legal in C++ to overload the operator++ so that
it decrements a value in your class?
What return value must conversion operators have in
their declarations?
Exercises
Write a SimpleCircle class declaration (only) with one member variable: itsRadius. Include a default constructor, a destructor, and accessor methods for
radius.
Using the class you created in
Exercise 1, write the implementation of the default constructor, initializing itsRadius with the value 5.
Using the same class, add a
second constructor that takes a value as its parameter and assigns that value
to itsRadius.
Create a prefix and postfix
increment operator for your SimpleCircle class
that increments itsRadius.
Change SimpleCircle to store itsRadius on the
free store, and fix the existing methods.
Provide a copy constructor for SimpleCircle.
Provide an assignment operator for SimpleCircle.
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.
BUG BUSTERS: What is wrong with
this implementation of the assignment operator?
SQUARE SQUARE
::operator=(const SQUARE & rhs)
0 comments:
Post a Comment