Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Beginning Visual C++ 2005 (2006) [eng]

.pdf
Скачиваний:
125
Добавлен:
16.08.2013
Размер:
18.66 Mб
Скачать

More on Classes

An assignment might seem very simple, but a couple of subtleties need further investigation. Note that you return a reference from the assignment operator function. It may not be immediately apparent why this is so(after all, the function does complete the assignment operation entirely, and the object on the right of the assignment is copied to that on the left. Superficially this would suggest that you don’t need to return anything, but you need to consider how the operator might be used in a little more depth.

There’s a possibility that you might need to use the result of an assignment operation on the right side of an expression. Consider a statement such as this:

motto1 = motto2 = motto3;

Because the assignment operator is right-associative, the assignment of motto3 to motto2 is carried out first, so this translates into the following statement:

motto1 = (motto2.operator=(motto3));

The result of the operator function call here is on the right of the equals sign, so the statement finally becomes this:

motto1.operator=(motto2.operator=(motto3));

If this is to work, you certainly have to return something. The call of the operator=() function between the parentheses must return an object that can be used as an argument to the other operator=() function call. In this case a return type of either CMessage or CMessage& would do it, so a reference is not mandatory in this situation, but you must at least return a CMessage object.

However, consider the following example:

(motto1 = motto2) = motto3;

This is perfectly legitimate code(the parentheses serve to make sure the leftmost assignment is carried out first. This translates into the following statement:

(motto1.operator=(motto2)) = motto3;

When you express the remaining assignment operation as the explicit overloaded function call, this ultimately becomes:

(motto1.operator=(motto2)).operator=(motto3);

Now you have a situation where the object returned from the operator=() function is used to call the operator=() function. If the return type is just CMessage, this is not legal, because a temporary copy of the original object is actually returned, and the compiler does not allow a member function call using a temporary object. In other words, the return value when the return type is CMessage is not an lvalue. The only way to ensure this sort of thing compiles and works correctly is to return a reference, which is an lvalue, so the only possible return type if want to allow fully flexible use of the assignment operator with your class objects is CMessage&.

Note that the native C++ language does not enforce any restrictions on the accepted parameter or return types for the assignment operator, but it makes sense to declare the operator in the way I have just described if you want your assignment operator functions to support normal C++ usage of assignment.

419

Chapter 8

The second subtlety you need to keep in mind is that each object already has memory for a string allocated, so the first thing that the operator function has to do is to delete the memory allocated to the first object and reallocate sufficient memory to accommodate the string belonging to the second object. After this is done, the string from the second object can be copied to the new memory now owned by the first.

There’s still a defect in this operator function. What if you were to write the following statement?

motto1 = motto1;

Obviously, you wouldn’t do anything as stupid as this directly, but it could easily be hidden behind a pointer, for instance, as in the following statement.

Motto1 = *pMess;

If the pointer pMess points to motto1 you essentially have the preceding assignment statement. In this case, the operator function as it stands would delete the memory for motto1, allocate some more memory based on the length of the string that has already been deleted and try to copy the old memory which, by then, could well have been corrupted. You can fix this with a check for identical left and right operands at the beginning of the function, so now the definition of the operator=() function would become this:

// Overloaded assignment operator for CMessage objects CMessage& operator=(const CMessage& aMess)

{

if(this == &aMess)

//

Check addresses, if equal

return *this;

//

return the 1st operand

//Release memory for 1st operand delete[] pmessage;

pmessage = new char[ strlen(aMess.pmessage) +1];

//Copy 2nd operand string to 1st

strcpy(this->pmessage, aMess.pmessage);

// Return a reference to 1st operand return *this;

}

This code assumes that the function definition appears within the class definition.

Try It Out

Overloading the Assignment Operator

Let’s put this together in a working example. We’ll add a function, called Reset(), to the class at the same time. This just resets the message to a string of asterisks.

//Ex8_05.cpp

//Overloaded copy operator perfection #include <iostream>

#include <cstring> using std::cout; using std::endl;

class CMessage

420

 

More on Classes

 

 

{

 

private:

 

char* pmessage;

// Pointer to object text string

public:

 

// Function to display a message

 

void ShowIt() const

 

{

 

cout << endl << pmessage;

 

}

 

//Function to reset a message to * void Reset()

{

char* temp = pmessage; while(*temp)

*(temp++) = ‘*’;

}

// Overloaded assignment operator for CMessage objects CMessage& operator=(const CMessage& aMess)

{

if(this == &aMess)

//

Check addresses, if equal

return *this;

//

return the 1st operand

//Release memory for 1st operand delete[] pmessage;

pmessage = new char[ strlen(aMess.pmessage) +1];

//Copy 2nd operand string to 1st

strcpy(this->pmessage, aMess.pmessage);

// Return a reference to 1st operand return *this;

}

// Constructor definition

CMessage(const char* text = “Default message”)

{

pmessage = new char[ strlen(text) +1 ]; // Allocate space for text

strcpy(pmessage, text);

// Copy text to new memory

}

 

// Destructor to free memory allocated by new

~CMessage()

 

{

 

cout << “Destructor called.”

// Just to track what happens

<< endl;

 

delete[] pmessage;

// Free memory assigned to pointer

}

 

};

421

Chapter 8

int main()

{

CMessage motto1(“The devil takes care of his own”); CMessage motto2;

cout << “motto2 contains - “; motto2.ShowIt();

cout << endl;

motto2 = motto1;

// Use new assignment operator

cout << “motto2 contains - “;

 

motto2.ShowIt();

 

cout << endl;

 

motto1.Reset();

// Setting motto1 to * doesn’t

 

// affect motto2

cout << “motto1 now contains - “;

 

motto1.ShowIt();

 

cout << endl;

 

cout << “motto2 still contains - “;

 

motto2.ShowIt();

 

cout << endl;

 

return 0;

 

}

You can see from the output of this program that everything works exactly as required, with no linking between the messages of the two objects, except where you explicitly set them equal:

motto2 contains - Default message motto2 contains -

The devil takes care of his own motto1 now contains -

*******************************

motto2 still contains -

The devil takes care of his own Destructor called.

Destructor called.

So let’s have another golden rule out of all of this:

Always implement an assignment operator if you allocate space dynamically for a data member of a class.

Having implemented the assignment operator, what happens with operations such as +=? Well, they don’t work unless you implement them. For each form of op= that you want to use with your class objects, you need to write another operator function.

422

More on Classes

Overloading the Addition Operator

Let’s look at overloading the addition operator for our CBox class. This is interesting because it involves creating and returning a new object. The new object is the sum (whatever you define that to mean) of the two CBox objects that are its operands.

So what do we want the sum of two boxes to mean? Well, there are quite a few legitimate possibilities but we’ll keep it simple here. Let’s define the sum of two CBox objects as a CBox object which is large enough to contain the other two boxes stacked on top of each other. You can do this by making the new object have an m_Length member, which is the larger of the m_Length members of the objects being added, and an m_Width member derived in a similar way. The m_Height member is the sum of the m_Height members of the two operand objects, so that the resultant CBox object can contain the other two CBox objects. This isn’t necessarily an optimal solution, but it is sufficient for our purposes. By altering the constructor, we’ll also

arrange that the m_Length member of a CBox object is always greater than or equal to the m_Width member.

Our version of the addition operation for boxes is easier to explain graphically, so it’s illustrated in Figure 8-4.

L = 30

maximum

L = 25

length

W = 20

box1

W = 25

box2

H = 15

H = 10

 

maximum

L = 30

width

 

W = 25 box1+box2

Sum of

H = 15+10

heights

= 25

Figure 8-4

423

 

Chapter 8

Because you need to get at the members of an object directly, you make the operator+() a member function. The declaration of the function member within the class definition is this:

CBox operator+(const CBox& aBox) const; // Function adding two CBox objects

You define the parameter as a reference to avoid unnecessary copying of the right argument when the function is called, and you make it a const reference because the function does not modify the argument. If you don’t declare the parameter as a const reference, the compiler does not allow a const object to be passed to the function, so it would then not be possible for the right operand of + to be a const CBox object. You also declare the function as const as it doesn’t change the object for which it is called. Without this, the left operand of + could not be a const CBox object.

The operator+() function definition is now as follows:

// Function to add two CBox objects

CBox CBox::operator+(const CBox& aBox) const

{

// New object has larger length and width, and sum of heights

return CBox(m_Length > aBox.m_Length ? m_Length:aBox.m_Length, m_Width > aBox.m_Width ? m_Width:aBox.m_Width, m_Height + aBox.m_Height);

}

You construct a local CBox object from the current object (*this) and the object that is passed as the argument, aBox. Remember that the return process makes a temporary copy of the local object and that is passed back to the calling function, not the local object discarded on return from the function.

Try It Out

Exercising Our Addition

You’ll be able to see how the overloaded addition operator in the CBox class works in this example:

//Ex8_06.cpp

//Adding CBox objects

#include <iostream>

// For stream I/O

using std::cout;

 

using std::endl;

 

class CBox

// Class definition at global scope

{

 

public:

// Constructor definition

CBox(double lv = 1.0, double wv = 1.0, double hv = 1.0): m_Height(hv)

{

m_Length = lv > wv? lv: wv;

//

Ensure

that

m_Width = wv < lv? wv: lv;

//

length

>= width

}

//Function to calculate the volume of a box double Volume() const

{

return m_Length*m_Width*m_Height;

}

//Operator function for ‘greater than’ which

424

More on Classes

// compares volumes of CBox objects.

int CBox::operator>(const CBox& aBox) const

{

return this->Volume() > aBox.Volume();

}

//Function to compare a CBox object with a constant int operator>(const double& value) const

{

return Volume() > value;

}

//Function to add two CBox objects

CBox operator+(const CBox& aBox) const

{

// New object has larger length & width, and sum of heights return CBox(m_Length > aBox.m_Length? m_Length:aBox.m_Length,

m_Width > aBox.m_Width? m_Width:aBox.m_Width, m_Height + aBox.m_Height);

}

// Function to show the dimensions of a box void ShowBox() const

{

cout << m_Length << “ “

<<m_Width << “ “

<<m_Height << endl;

}

private:

double m_Length; double m_Width; double m_Height;

//Length of a box in inches

//Width of a box in inches

//Height of a box in inches

};

int operator>(const double& value, const CBox& aBox); // Function prototype

int main()

{

CBox smallBox(4.0, 2.0, 1.0); CBox mediumBox(10.0, 4.0, 2.0); CBox aBox;

CBox bBox;

aBox = smallBox + mediumBox; cout << “aBox dimensions are “; aBox.ShowBox();

bBox = aBox + smallBox + mediumBox; cout << “bBox dimensions are “; bBox.ShowBox();

return 0;

}

// Function comparing a constant with a CBox object

425

Chapter 8

int operator>(const double& value, const CBox& aBox)

{

return value > aBox.Volume();

}

You’ll be using the CBox class definition again a few pages down the road in this chapter, so make a note that you’ll want to return to this point in the book.

How It Works

In this example I have changed the CBox class members a little. The destructor has been deleted as it isn’t necessary for this class, and the constructor has been modified to ensure that the m_Length member isn’t less than the m_Width member. Knowing that the length of a box is always at least as big as the width makes the add operation a bit easier. I’ve also added the ShowBox() function to output the dimensions of a CBox object. Using this we’ll be able to verify that our overloaded add operation is working as we expect.

The output from this program is:

aBox dimensions are 10 4 3 bBox dimensions are 10 4 6

This seems to be consistent with the notion of adding CBox objects that we have defined and, as you can see, the function also works with multiple add operations in an expression. For the computation of bBox, the overloaded addition operator is called twice.

You could equally well have implemented the add operation for the class as a friend function. Its prototype is this:

friend CBox operator+(const CBox& aBox, const CBox& bBox);

The process for producing the result would be much the same, except that you’d need to use the direct member selection operator to obtain the members for both the arguments to the function. It would work just as well as the first version of the operator function.

Overloading the Increment and Decrement Operators

I’ll briefly introduce the mechanism for overloading the increment and decrement operators in a class because they have some special characteristics that make them different from other unary operators. You need a way to deal with the fact that the ++ and -- operators come in a prefix and postfix form, and the effect is different depending on whether the operator is applied in its prefix or postfix form. In native C++ the overloaded operator is different for the prefix and postfix forms of the increment and decrement operators. Here’s how they would be defined in a class with the name Length, for example:

class Length

{

private:

double len;

// Length value for the class

public:

 

Length& operator++();

// Prefix increment operator

426

 

More on Classes

 

 

const Length operator++(int);

// Postfix increment operator

Length& operator--();

// Prefix decrement operator

const Length operator--(int);

// Postfix decrement operator

// rest of the class...

 

}

This simple class assumes a length is stored just as a value of type double. In reality, you might make a length class more sophisticated than this, but it serves to illustrate how you overload the increment and decrement operators.

The primary way the prefix and postfix forms of the overloaded operators are differentiated is by the parameter list; for the prefix form there are no parameters and for the postfix form there is a parameter of type int. The parameter in the postfix operator function is only to distinguish it from the prefix form and is otherwise unused in the function implementation.

The prefix increment and decrement operators increment the operand before its value is used in an expression so you just return a reference to the current object after it has been incremented or decremented. With the prefix forms, the operand is incremented after its current value is used in an expression. This is achieved by creating a new object that is a copy of the current object before incrementing the current object and returning the copy after the current object has been modified.

Class Templates

You saw in Chapter 6 that you could define a function template that would automatically generate functions varying in the type of arguments accepted, or in the type of value returned. C++ has a similar mechanism for classes. A class template is not in itself a class; it’s a sort of “recipe” for a class that is used by the compiler to generate the code for a class. As you can see from Figure 8-5, it’s like the function template — you determine the class that you want generated by specifying your choice of type for the parameter (T in this case) that appears between the angled brackets in the template. Doing this generates a particular class referred to as an instance of the class template. The process of creating a class from a template is described as instantiating the template.

An appropriate class definition is generated when you instantiate an object of a template class for a particular type, so you can generate any number of different classes from one class template. You’ll get a good idea of how this works in practice by looking at an example.

427

Chapter 8

T is a parameter for which you supply an argument value that is a type.

Each different type value argument you specify creates a new class.

T specified as int

class CExample

{

int m_Value;

...

}

template<class T>

 

 

 

 

 

 

 

 

 

class CExample

class CExample

 

 

 

 

 

 

 

 

{

{

 

 

 

 

 

 

 

 

double m_Value;

 

T specified as double

 

 

T m_Value;

 

 

 

 

 

 

 

...

...

 

 

 

 

 

 

 

 

}

}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

class template

 

 

 

 

 

T specified as CBox

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

class CExample

 

 

The class is created

{

 

 

CBox m_Value;

 

 

by using your value in

 

 

place of T in the

...

 

 

template

}

 

 

 

 

 

 

Figure 8-5

class instances

Defining a Class Template

I’ll choose a simple example to illustrate how you define and use a class template, and I won’t complicate things by worrying too much about errors that can arise if it’s misused. Suppose you want to define classes that can store a number of data samples of some kind, and each class is to provide a Max() function to determine the maximum sample value of those stored. This function is similar to the one you saw in the function template discussion in Chapter 6. You can define a class template, which generates a class CSamples to store samples of whatever type you want.

template <class T> class CSamples

{

public:

// Constructor definition to accept an array of samples CSamples(const T values[], int count)

{

m_Free = count < 100? count:100; // Don’t exceed the array

for(int i = 0; i < m_Free; i++)

m_Values[i] = values[i];

// Store count number of samples

}

 

// Constructor to accept a single sample

CSamples(const T& value)

 

{

 

m_Values[0] = value;

// Store the sample

428