In this chapter you’ll learn about the following:
• Operator overloading
• Friend functions
• Overloading the <<
operator for output
• State members
• Using rand()
to generate random values
• Automatic conversions and type casts for classes
• Class conversion functions
C++ classes are feature-rich, complex, and powerful. In Chapter 10, “Objects and Classes,” you began a journey toward object-oriented programming by learning to define and use a simple class. You saw how a class defines a data type by defining the type of data to be used to represent an object and by also defining, through member functions, the operations that can be performed with that data. And you learned about two special member functions, the constructor and the destructor, that manage creating and discarding objects made to a class specification. This chapter takes you a few steps further in the exploration of class properties, concentrating on class design techniques rather than on general principles. You’ll probably find some of the features covered here straightforward and some a bit more subtle. To best understand these new features, you should try the examples and experiment with them: What happens if you use a regular argument instead of a reference argument for this function? What happens if you leave something out of a destructor? Don’t be afraid to make mistakes; usually you can learn more from unraveling an error than by doing something correctly but by rote. (However, don’t assume that a maelstrom of mistakes inevitably leads to incredible insight.) In the end, you’ll be rewarded with a fuller understanding of how C++ works and of what C++ can do for you.
This chapter starts with operator overloading, which lets you use standard C++ operators such as =
and +
with class objects. Then it examines friends, the C++ mechanism for letting nonmember functions access private data. Finally, it looks at how you can instruct C++ to perform automatic type conversions with classes. As you go through this chapter and Chapter 12, “Classes and Dynamic Memory Allocation,” you’ll gain a greater appreciation of the roles class constructors and class destructors play. Also you’ll see some of the stages you may go through as you develop and improve a class design.
One difficulty with learning C++, at least by the time you’ve gotten this far into the subject, is that there is an awful lot to remember. And it’s unreasonable to expect to remember it all until you’ve logged enough experience on which to hang your memories. Learning C++, in this respect, is like learning a feature-laden word processor or spreadsheet program. No one feature is that daunting, but, in practice, most people really know well only those features they use regularly, such as searching for text or italicizing. You may recall having read somewhere how to generate alternative characters or create a table of contents, but those skills probably won’t be part of your daily repertoire until you face a situation in which you need them frequently. Probably the best approach to absorbing the wealth of material in this chapter is to begin incorporating just some of these new features into your own C++ programming. As your experiences enhance your understanding and appreciation of these features, you can begin adding other C++ features. As Bjarne Stroustrup, the creator of C++, suggested at a C++ conference for professional programmers: “Ease yourself into the language. Don’t feel you have to use all of the features, and don’t try to use them all on the first day.”
Let’s look at a technique for giving object operations a prettier look. Operator overloading is an example of C++ polymorphism. In Chapter 8, “Adventures in Functions,” you saw how C++ enables you to define several functions that have the same name, provided that they have different signatures (argument lists). That is called function overloading, or functional polymorphism. Its purpose is to let you use the same function name for the same basic operation, even though you apply the operation to different data types. (Imagine how awkward English would be if you had to use a different verb form for each different type of object—for example, lift_lft your left foot, but lift_sp your spoon.) Operator overloading extends the overloading concept to operators, letting you assign multiple meanings to C++ operators. Actually, many C++ (and C) operators already are overloaded. For example, the *
operator, when applied to an address, yields the value stored at that address. But applying *
to two numbers yields the product of the values. C++ uses the number and type of operands to decide which action to take.
C++ lets you extend operator overloading to user-defined types, permitting you, say, to use the +
symbol to add two objects. Again, the compiler uses the number and type of operands to determine which definition of addition to use. Overloaded operators can often make code look more natural. For example, a common computing task is adding two arrays. Usually, this winds up looking like the following for
loop:
for (int i = 0; i < 20; i++)
evening[i] = sam[i] + janet[i]; // add element by element
But in C++, you can define a class that represents arrays and that overloads the +
operator so that you can do this:
evening = sam + janet; // add two array objects
This simple addition notation conceals the mechanics and emphasizes what is essential, and that is another goal of OOP.
To overload an operator, you use a special function form called an operator function. An operator function has the following form, where op
is the symbol for the operator being overloaded:
operatorop(argument-list)
For example, operator+()
overloads the +
operator and operator*()
overloads the *
operator. The op
has to be a valid C++ operator; you can’t just make up a new symbol. For example, you can’t have an operator@()
function because C++ has no @
operator. But the operator[]()
function would overload the []
operator because []
is the array-indexing operator. Suppose, for example, that you have a Salesperson
class for which you define an operator+()
member function to overload the +
operator so that it adds sales figures of one salesperson object to another. Then, if district2
, sid
, and sara
are all objects of the Salesperson
class, you can write this equation:
district2 = sid + sara;
The compiler, recognizing the operands as belonging to the Salesperson
class, replaces the operator with the corresponding operator function:
district2 = sid.operator+(sara);
The function then uses the sid
object implicitly (because it invoked the method) and the sara
object explicitly (because it’s passed as an argument) to calculate the sum, which it then returns. Of course, the nice part is that you can use the nifty +
operator notation instead of the clunky function notation.
C++ imposes some restrictions on operator overloading, but they’re easiest to understand after you’ve seen how overloading works. So let’s develop a few examples to clarify the process and then discuss the limitations.
If you worked on the Priggs account for 2 hours 35 minutes in the morning and 2 hours 40 minutes in the afternoon, how long did you work altogether on the account? Here’s an example where the concept of addition makes sense, but the units that you are adding (a mixture of hours and minutes) don’t match a built-in type. Chapter 7, “Functions: C++’s Programming Modules,” handles a similar case by defining a travel_time
structure and a sum()
function for adding such structures. Now let’s generalize that to a Time
class, using a method to handle addition. Let’s begin with an ordinary method, called Sum()
, and then see how to convert it to an overloaded operator. Listing 11.1 shows the class declaration.
// mytime0.h -- Time class before operator overloading
#ifndef MYTIME0_H_
#define MYTIME0_H_
class Time
{
private:
int hours;
int minutes;
public:
Time();
Time(int h, int m = 0);
void AddMin(int m);
void AddHr(int h);
void Reset(int h = 0, int m = 0);
Time Sum(const Time & t) const;
void Show() const;
};
#endif
The Time
class provides methods for adjusting and resetting times, for displaying time values, and for adding two times. Listing 11.2 shows the methods definitions; note how the AddMin()
and Sum()
methods use integer division and the modulus operator to adjust the minutes
and hours
values when the total number of minutes exceeds 59. Also because the only iostream
feature used is cout
and because it is used only once, it seems economical to use std::cout
rather than use the whole std
namespace.
// mytime0.cpp -- implementing Time methods
#include <iostream>
#include "mytime0.h"
Time::Time()
{
hours = minutes = 0;
}
Time::Time(int h, int m )
{
hours = h;
minutes = m;
}
void Time::AddMin(int m)
{
minutes += m;
hours += minutes / 60;
minutes %= 60;
}
void Time::AddHr(int h)
{
hours += h;
}
void Time::Reset(int h, int m)
{
hours = h;
minutes = m;
}
Time Time::Sum(const Time & t) const
{
Time sum;
sum.minutes = minutes + t.minutes;
sum.hours = hours + t.hours + sum.minutes / 60;
sum.minutes %= 60;
return sum;
}
void Time::Show() const
{
std::cout << hours << " hours, " << minutes << " minutes";
}
Consider the code for the Sum()
function. Note that the argument is a reference but that the return type is not a reference. The reason for making the argument a reference is efficiency. The code would produce the same results if the Time
object were passed by value, but it’s usually faster and more memory-efficient to just pass a reference.
However, the return value cannot be a reference. The reason is that the function creates a new Time
object (sum
) that represents the sum of the other two Time
objects. Returning the object, as this code does, creates a copy of the object that the calling function can use. If the return type were Time &
, however, the reference would be to the sum
object. But the sum
object is a local variable and is destroyed when the function terminates, so the reference would be a reference to a non-existent object. Using a Time
return type, however, means the program constructs a copy of sum
before destroying it, and the calling function gets the copy.
Don’t return a reference to a local variable or another temporary object. When the function terminates and the local variable or temporary object disappears, the reference becomes a reference to non-existent data.
Finally, Listing 11.3 tests the time summation part of the Time
class.
// usetime0.cpp -- using the first draft of the Time class
// compile usetime0.cpp and mytime0.cpp together
#include <iostream>
#include "mytime0.h"
int main()
{
using std::cout;
using std::endl;
Time planning;
Time coding(2, 40);
Time fixing(5, 55);
Time total;
cout << "planning time = ";
planning.Show();
cout << endl;
cout << "coding time = ";
coding.Show();
cout << endl;
cout << "fixing time = ";
fixing.Show();
cout << endl;
total = coding.Sum(fixing);
cout << "coding.Sum(fixing) = ";
total.Show();
cout << endl;
return 0;
}
Here is the output of the program in Listings 11.1, 11.2, and 11.3:
planning time = 0 hours, 0 minutes
coding time = 2 hours, 40 minutes
fixing time = 5 hours, 55 minutes
coding.Sum(fixing) = 8 hours, 35 minutes
3.145.109.234