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

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

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

8

More on Classes

In this chapter, you will extend your knowledge of classes by understanding how you can make your class objects work more like the basic types in C++. You will learn about:

Class destructors and when and why they are necessary

How to implement a class destructor

How to allocate data members of a native C++ class in the free store and how to delete them when they are no longer required

When you must write a copy constructor for a class

Unions and how they can be used

How to make objects of your class work with C++ operators such as + or *

Class templates and how you define and use them

How to overload operators in C++/CLI classes

Class Destructors

Although this section heading refers to destructors, it’s also about dynamic memory allocation. When you allocate memory in the free store for class members, you are invariably obliged to make use of a destructor, in addition to a constructor of course, and, as you’ll see later in this chapter, using dynamically allocated class members also require you to write your own copy constructor.

What Is a Destructor?

A destructor is a function that destroys an object when it is no longer required or when it goes out of scope. The class destructor is called automatically when an object goes out of scope. Destroying an object involves freeing the memory occupied by the data members of the object (except for static members which continue to exist even when there are no class objects in existence). The destructor for a class is a member function with the same name as the class, preceded by a tilde

Chapter 8

(~). The class destructor doesn’t return a value and doesn’t have parameters defined. For the CBox class, the prototype of the class destructor is:

~CBox();

// Class destructor prototype

Because a destructor has no parameters, there can only ever be one destructor in a class.

It’s an error to specify a return value or parameters for a destructor.

The Default Destructor

All the objects that you have been using up to now have been destroyed automatically by the default destructor for the class. The default destructor is always generated automatically by the compiler if you do not define your own class destructor. The default destructor doesn’t delete objects or object members that have been allocated in the free store by the operator new. If space for class members has been allocated dynamically in a contructor, you must define your own destructor that explicitly use the delete operator to release the memory that has been allocated by the constructor using the operator new, just as you would with ordinary variables. You need some practice in writing destructors, so let’s try it out.

Try It Out

A Simple Destructor

To get an appreciation of when the destructor for a class is called, you can include a destructor in the class CBox. Here’s the definition of the example including the CBox class with a destructor:

//Ex8_01.cpp

//Class with an explicit destructor #include <iostream>

using std::cout; using std::endl;

class CBox

// Class definition at global scope

{

 

public:

 

//Destructor definition ~CBox()

{

cout << “Destructor called.” << endl;

}

//Constructor definition

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

{

cout << endl << “Constructor called.”;

}

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

{

return m_Length*m_Width*m_Height;

}

//Function to compare two boxes which returns true

400

More on Classes

// if the first is greater that the second, and false otherwise int compare(CBox* pBox) const

{

return this->Volume() > pBox->Volume();

}

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

};

// Function to demonstrate the CBox class destructor in action

int main()

 

{

 

CBox boxes[5];

// Array of CBox objects declared

CBox cigar(8.0, 5.0, 1.0);

// Declare cigar box

CBox match(2.2, 1.1, 0.5);

// Declare match box

CBox* pB1 = &cigar;

// Initialize pointer to cigar object address

CBox* pB2 = 0;

// Pointer to CBox initialized to null

cout <<

endl

 

<<

“Volume of cigar is “

<< pB1->Volume();

// Volume of obj. pointed to

pB2 = boxes;

// Set to address of array

boxes[2] = match;

// Set 3rd element to match

cout << endl

 

<<“Volume of boxes[2] is “

<<(pB2 + 2)->Volume(); // Now access thru pointer

cout << endl; return 0;

}

How It Works

The only thing that the CBox class destructor does is to display a message showing that it was called. The output is:

Constructor called.

Constructor called.

Constructor called.

Constructor called.

Constructor called.

Constructor called.

Constructor called.

Volume of cigar is 40

Volume of boxes[2] is 1.21

Destructor called.

Destructor called.

Destructor called.

Destructor called.

Destructor called.

Destructor called.

Destructor called.

401

Chapter 8

You get one call of the destructor at the end of the program for each of the objects that exist. For each constructor call that occurred, there’s a matching destructor call. You don’t need to call the destructor explicitly here. When an object of a class goes out of scope, the compiler arranges for the destructor for the class to be called automatically. In our example, the destructor calls occur after main() has finished executing, so it’s quite possible for an error in a destructor to cause a program to crash after main() has safely terminated.

Destructors and Dynamic Memory Allocation

You will find that you often want to allocate memory for class data members dynamically. You can use the operator new in a constructor to allocate space for an object member. In such a case, you must assume responsibility for releasing the space when the object is no longer required by providing a suitable destructor. Let’s first define a simple class where we can do this.

Suppose you want to define a class where each object is a message of some description, for example, a text string. The class should be as memory efficient as possible so, rather than defining a data member as a char array big enough to hold the maximum length string that you might require, you’ll allocate memory in the free store for the message when an object is created. Here’s the class definition:

//Listing 08_01

 

class CMessage

 

{

 

private:

 

char* pmessage;

// Pointer to object text string

public:

 

//Function to display a message void ShowIt() const

{

cout << endl << pmessage;

}

//Constructor definition

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

{

pmessage = new char[strlen(text) + 1]; strcpy(pmessage, text);

//Allocate space for text

//Copy text to new memory

}

~CMessage();

// Destructor prototype

};

This class has only one data member defined, pmessage, which is a pointer to a text string. This is defined in the private section of the class, so that it can’t be accessed from outside the class.

In the public section, you have the ShowIt() function that outputs a CMessage object to the screen. You also have the definition of a constructor and you have the prototype for the class destructor, ~CMessage(), which I’ll come to in a moment.

402

More on Classes

The constructor for the class requires a string as an argument, but if none is passed, it uses the default string that is specified for the parameter. The constructor obtains the length of the string supplied as the argument, excluding the terminating NULL, by using the library function strlen(). For the constructor to use this library function, there must be a #include statement for the <cstring> header file. The constructor determines the number of bytes of memory necessary to store the string in the free store by adding 1 to the value that the function strlen() returns.

Of course, if the memory allocation fails, an exception will be thrown that will terminate the program. If you want to manage such a failure to provide a more graceful end to the program, you would catch the exception within the constructor code. (See Chapter 6 for information on handling out-of-memory conditions.)

Having obtained the memory for the string using the operator new, you use the strcpy() library function that is also declared in the <cstring> header file to copy the string supplied as the argument to the constructor into the memory allocated for it. The strcpy() function copies the string specified by the second pointer argument to the address contained in the first pointer argument.

You now need to write a class destructor that frees up the memory allocated for a message. If you don’t provide a destructor for the class, there’s no way to delete the memory allocated for an object. If you use this class as it stands in a program where a large number of CMessage objects are created, the free store is gradually eaten away until the program fails. It’s easy for this to occur in circumstances where it may not be obvious that it is happening. For example, if you create a temporary CMessage object in a function that is called many times in a program, you might assume that the objects are being destroyed at the return from the function. You’d be right about that, of course, but the free store memory is released. Thus for each call of the function, more of the free store is occupied by memory for discarded CMessage objects.

The code for the CMessage class destructor is as follows:

//Listing 08_02

//Destructor to free memory allocated by new CMessage::~CMessage()

{

cout <<

“Destructor called.”

//

Just

to track what happens

<<

endl;

 

 

 

delete[] pmessage;

//

Free

memory assigned to pointer

}

Because you’re defining the destructor outside of the class definition, you must qualify the name of the destructor with the class name, CMessage. All the destructor does is display a message so that you can see what’s going on and then use the delete operator to free the memory pointed to by the member pmessage. Note that you have to include the square brackets with delete because you’re deleting an array (of type char).

Try It Out

Using the Message Class

You can exercise the CMessage class with a little example:

//Ex8_02.cpp

//Using a destructor to free memory

#include <iostream>

// For stream I/O

403

Chapter 8

#include <cstring>

// For strlen() and strcpy()

using std::cout;

 

using std::endl;

 

//Put the CMessage class definition here (Listing 08_01)

//Put the destructor definition here (Listing 08_02)

int main()

{

// Declare object

CMessage motto(“A miss is as good as a mile.”);

// Dynamic object

CMessage* pM = new CMessage(“A cat can look at a queen.”);

motto.ShowIt();

// Display 1st message

pM->ShowIt();

// Display 2nd message

cout << endl;

 

// delete pM;

// Manually delete object created with new

return 0;

 

}

Don’t forget to replace the comments in the code with the CMessage class and destructor definitions from the previous section; it won’t compile without this.

How It Works

At the beginning of main(), you declare and define an initialized CMessage object, motto, in the usual manner. In the second declaration you define a pointer to a CMessage object, pM, and allocate memory for the CMessage object that is pointed to by using the operator new. The call to new invokes the CMessage class constructor, which has the effect of calling new again to allocate space for the message text pointed to by the data member pmessage. If you build and execute this example, it produces the following output:

A miss is as good as a mile.

A cat can look at a queen.

Destructor called.

You have only one destructor call recorded in the output, even though you created two CMessage objects. I said earlier that the compiler doesn’t take responsibility for objects created in the free store. The compiler arranged to call your destructor for the object motto because this is a normal automatic object, even though the memory for the data member was allocated in the free store by the constructor. The object pointed to by pM is different. You allocated memory for the object in the free store, so you have to use delete to remove it. You need to uncomment the following statement that appears just before the return statement in main():

// delete pM;

// Manually delete object created with new

404

More on Classes

If you run the code now, it produces this output:

A miss is as good as a mile.

A cat can look at a queen.

Destructor called.

Destructor called.

Now you get an extra call of your destructor. This is surprising in a way. Clearly, delete is only dealing with the memory allocated by the call to new in the function main(). It only freed the memory pointed to by pM. Because your pointer pM points to a CMessage object (for which a destructor has been defined), delete also calls your destructor to allow you to release the memory for the members of the object. So when you use delete for an object created dynamically with new, it always calls the destructor for the object before releasing the memory that the object occupies.

Implementing a Copy Constructor

When you allocate space for class members dynamically, there are demons lurking in the free store. For the CMessage class, the default copy constructor is woefully inadequate. Suppose you write these statements:

CMessage motto1(“Radiation fades your genes.”);

CMessage motto2(motto1);

// Calls the default copy constructor

The effect of the default copy constructor is to copy the address that is stored in the pointer member of the class from motto1 to motto2 because the copying process implemented by the default copy constructor involves simply copying the values stored in the data members of the original object to the new object. Consequently, only one text string is shared between the two objects, as Figure 8-1 illustrates.

If the string is changed from either of the objects, it changes for the other as well because both objects share the same string. If motto1 is destroyed, the pointer in motto2 is pointing at a memory area that has been released, and may now be used for something else, so chaos will surely ensue. Of course, the same problem arises if motto2 is deleted; motto1 would then contain a member pointing to a nonexistent string.

The solution is to supply a class copy constructor to replace the default version. You could implement this in the public section of the class as follows:

CMessage(const CMessage& initM) // Copy Constructor definition

{

// Allocate space for text

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

// Copy text to new memory strcpy(pmessage, initM.pmessage);

}

405

Chapter 8

CMessage motto1 ( “ Radiation fades your genes .” ) ;

motto1

pmessage address

 

 

 

 

Radiation fades your genes.

 

CMessage motto2 (motto1) ;

// Calls

the default copy constructor

 

 

 

 

 

motto1

copy

motto2

 

 

 

 

pmessage

 

 

 

 

 

 

 

 

 

 

 

pmessage

address

 

 

 

address

 

 

 

 

 

 

 

 

 

 

Radiation fades your genes.

Figure 8-1

Remember from the previous chapter that, to avoid an infinite spiral of calls to the copy constructor, the parameter must be specified as a const reference. This copy constructor first allocates enough memory to hold the string in the object initM, storing the address in the data member of the new object, and then copies the text string from the initializing object. Now, the new object is identical to, but quite independent of, the old one.

Just because you don’t initialize one CMessage class object with another, don’t think that you’re safe and need not bother with the copy constructor. Another monster lurks in the free store that can emerge to bite you when you least expect it. Consider the following statements:

CMessage thought(“Eye awl weighs yews my spell checker.”);

DisplayMessage(thought);

// Call a function to output a message

where the function DisplayMessage() is defined as:

void DisplayMessage(CMessage localMsg)

{

cout << endl << “The message is: “

<< localMsg.ShowIt(); return;

}

406

More on Classes

Looks simple enough, doesn’t it? What could be wrong with that? A catastrophic error, that’s what! What the function DisplayMessage() does is actually irrelevant. The problem lies with the parameter. The parameter is a CMessage object so the argument in a call is passed by value. With the default copy constructor, the sequence of events is as follows:

1.The object thought is created with the space for the message “Eye awl weighs yews my spell checker” allocated in the free store.

2.The function DisplayMessage() is called and, because the argument is passed by value, a copy, localMsg, is made using the default copy constructor. Now the pointer in the copy points to the same string in the free store as the original object.

3.At the end of the function, the local object goes out of scope, so the destructor for the CMessage class is called. This deletes the local object (the copy) by deleting the memory pointed to by the pointer pmessage.

4.On return from the function DisplayMessage(), the pointer in the original object, thought, still points to the memory area that has just been deleted. Next time you try to use the original object (or even if you don’t, since it needs to be deleted sooner or later) your program will behave in weird and mysterious ways.

Any call to a function that passes by value an object of a class that has a member defined dynamically will cause problems. So, out of this, you have an absolutely 100 percent, 24 carat golden rule:

If you allocate space for a member of a native C++ class dynamically, always implement a copy constructor.

Sharing Memor y Between Variables

As a relic of the days when 64K was quite a lot of memory, you have a facility in C++ which allows more than one variable to share the same memory (but obviously not at the same time). This is called a union, and there are four basic ways in which you can use one:

You can use it so that a variable A occupies a block of memory at one point in a program, which is later occupied by another variable B of a different type, because A is no longer required.

I recommend that you don’t do this. It’s not worth the risk of error that is implicit in such an arrangement. You can achieve the same effect by allocating memory dynamically.

You could have a situation in a program where a large array of data is required, but you don’t know in advance of execution what the data type will be — it will be determined by the input data. I also recommend that you don’t use unions in this case because you can achieve the same result using a couple of pointers of different types and again allocating the memory dynamically.

A third possible use for a union is the one that you may need now and again — when you want to interpret the same data in two or more different ways. This could happen when you have a variable that is of type long, and you want to treat it as two values of type short. Windows sometimes packages two short values in a single parameter of type long passed to a function. Another instance arises when you want to treat a block of memory containing numeric data as a string of bytes, just to move it around.

407

Chapter 8

You can use a union as a means of passing an object or a data value around where you don’t know in advance what its type is going to be. The union can provide for storing any one of the possible range of types that you might have.

Defining Unions

You define a union using the keyword union. It is best understood by taking an example of a definition:

union shareLD

// Sharing memory between long and double

{

 

double dval;

 

long lval;

 

};

 

This defines a union type shareLD that provides for the variables of type long and double to occupy the same memory. The union type name is usually referred to as a tag name. This statement is similar to a class definition in that you haven’t actually defined a union instance yet, so you don’t have any variables at this point. After the union type has been defined, you can define instances of a union in a declaration. For example:

shareLD myUnion;

This defined an instance of the union type, shareLD, that you defined previously. You could also have defined myUnion by including it in the union definition statement:

union shareLD

// Sharing memory between long and double

{

 

double dval;

 

long lval;

 

} myUnion;

 

To refer to a member of the union, you use the direct member selection operator (the period) with the union instance name, just as you have done when accessing members of a class. So, you could set the long variable lval to 100 in the union instance MyUnion with this statement:

myUnion.lval = 100;

// Using a member of a union

Using a similar statement later in a program to initialize the double variable dval overwrites lval. The basic problem with using a union to store different types of values in the same memory is that because of the way a union works, you also need some means of determining which of the member values is current. This is usually achieved by maintaining another variable that acts as an indicator of the type of value stored.

A union is not limited to sharing between two variables. If you want, you can share the same memory between several variables. The memory occupied by the union is that required by its largest member. For example, suppose you define this union:

union shareDLF

{

double dval;

408