• No results found

As mentioned earlier, a dangling pointer is a pointer variable that no longer references a valid object. Four situations can cause dangling pointers, but we will cover only three of them because we consider the fourth one to be an unsafe practice. 2

As Section C2.3.1 discusses, if you do not set a pointer variable to nullptr after using delete , you leave the pointer dangling.

Similarly, if you declare a pointer variable but do not assign it a value, the result is a dangling pointer. As with any C++ variable, when C++ creates a pointer variable, its value is unde- fi ned. That is, C++ does not automatically initialize or clear the memory that a variable represents. The compiler and the programmer have no way to check that the value in the pointer variable actually points to an object in the free store. That value might simply be whatever was in the memory when the pointer was created. For example, suppose that you write statements such as

MagicBox< int>* myMagicBoxPtr; myMagicBoxPtr->getItem();

Here, the pointer variable myMagicBoxPtr is not assigned an object to point to. When the method getItem is called, the program will abort—usually with a segment fault error— because the program treats the value in myMagicBoxPtr as the address of a MagicBox<int> object and tries to fi nd the getItem method at that address. Since it is very unlikely that a MagicBox<int> object happens to be there, the program ends abnormally.

If you need to create a pointer variable but do not have an object for it to point to, you should always set it to nullptr :

MagicBox<int>* myMagicBoxPtr = nullptr;

Then your code can compare the pointer variable to nullptr to see whether it points to a valid object on the free store, as in the following example:

if (myMagicBoxPtr !=nullptr) myMagicBoxPtr->getItem();

If the pointer is not nullptr , you can use the pointer to call a method.

The third situation that can cause a dangling pointer is subtle, and the best way to guard against it is careful programming. Consider the following statements that we saw earlier when dis- cussing memory leaks:

MagicBox<string>* myBoxPtr = new MagicBox<string>(); MagicBox<string>* yourBoxPtr = myBoxPtr;

Executing this code results in the memory confi guration shown in Figure C2-7 , where both yourBoxPtr and myBoxPtr point to the same object. We say that yourBoxPtr is an alias of myBoxPtr since they both refer to the same object. There is nothing wrong with this code so far, and it does not have any memory leaks.

Suppose we then execute the following statements: delete myBoxPtr;

myBoxPtr = nullptr; yourBoxPtr->getItem();

2 We do not use & as the “address of ” operator in this textbook. As a result, pointer variables can be assigned only to variables in the free store, so a function cannot return a pointer to a local variable that no longer exists.

Situations that can cause a dangling pointer

Here we try to practice safe and secure programming by setting myBoxPtr to nullptr after delet- ing the object that it points to. But the callyourBoxPtr->getItem() results in the program aborting. What happened? Figure C2-8 shows the state of memory just prior to this call. As you can see, the object pointed to by myBoxPtr was deleted, as it should have been. The problem arises becauseyourBoxPtr still references the object’s location in the free store, even though the object no longer exists. Since the object no longer exists, yourBoxPtr is a dangling pointer and the program aborts when we try to call a method on that object.

Programming Tip:

How to avoid dangling pointers

• Set pointer variables to nullptr either initially or when you no longer need them. If a class has a pointer variable as a data fi eld, the constructor should always initialize that data fi eld, to point either to an object or to nullptr . The class GoodMemory in Listing C2-3 demonstrates this safeguard.

• Test whether a pointer variable contains nullptr before using it to call a method. • Try to reduce the use of aliases in your program. As you will see, that is not always

possible or desirable in certain situations.

• Do not delete an object in the free store until you are certain that no other alias needs to use it.

• Set all aliases that reference a deleted object to nullptr when the object is deleted.

FIGURE C2-7 Two pointers referencing (pointing to) the same object myBoxPtr

Object yourBoxPtr

FIGURE C2-8 Example of a dangling pointer

myBoxPtr

nullptr

yourBoxPtr

C2.4

Virtual Methods and Polymorphism

Now that we have some of the basics of pointers behind us, we can dive into the implementation of polymorphism. To allow the compiler to perform the late binding necessary for polymorphism, you must declare the methods in the base class as virtual. In C++ Interlude 1, we began a discussion of

Virtual Methods and Polymorphism 129

An example of late binding

virtual methods—methods that use the keyword virtual to indicate that they can be overridden. In the example in Section C2.2, PlainBox is the base class, which we defi ned in Listing C1-3 of C++ Interlude 1. The code that we wrote then did not behave as we desired. To correct that problem, we must declare as virtual the methods of PlainBox that we want other classes to override, as Listing C2-4 shows. Notice that only the header fi le of the base class ( PlainBox ) needs to be revised. Declaring the methodssetItem and getItem as virtual makes it possible for the method code to be bound late.

LISTING C2-4 Revised header fi le for the class PlainBox /** @file PlainBox.h */

#ifndef _PLAIN_BOX #define _PLAIN_BOX

template< class ItemType> ; // Indicates this is a template // Declaration for the class PlainBox

class PlainBox { private: // Data field ItemType item; public: // Default constructor PlainBox(); // Parameterized constructor PlainBox( const ItemType& theItem);

// Mutator method that can change the value of the data field virtual void setItem( const ItemType& theItem);

// Accessor method to get the value of the data field virtual ItemType getItem() const;

}; // end PlainBox

#include "PlainBox.cpp" // Include the implementation file #endif

To fully implement late binding, we must create the variables in the free store and use pointers to reference them. Thus, we must also change the code from our fi rst example of early binding in Section C2.2 to

string specialItem = "Riches beyond compare!"; string otherItem = "Hammer";

PlainBox<string>* myPlainBoxPtr = new PlainBox<string>(); placeInBox(myPlainBoxPtr, specialItem);

MagicBox<string>* myMagicBoxPtr =new MagicBox<string>(); placeInBox(myMagicBoxPtr, otherItem);

placeInBox(myMagicBoxPtr, specialItem); cout << myMagicBoxPtr->getItem() << endl;

Next, we must change the function placeInBox to accept a pointer to a PlainBox<string> object: void placeInBox(PlainBox<string>* theBox, string theItem)

{

theBox->setItem(theItem); } // end placeInBox

The last change is to free the memory used by the variables in the free store by adding the following statements to the program:

delete myPlainBoxPtr; myPlainBoxPtr = nullptr; delete myMagicBoxPtr; myMagicBoxPtr = nullptr;

Our function placeInBox now will call the correct version of setItem according to the type of box pointed to by its fi rst argument.

The use of virtual methods has a signifi cant impact on the future use of a class. Imagine that you had compiled the class PlainBox and its implementation before you wrote the derived class MagicBox . If you then wrote MagicBox , assuming access to the compiled class PlainBox , you could override getItem because it is virtual in PlainBox . As a result, you would change the behavior of getItem for instances of MagicBox , even though PlainBox was already compiled. That is, classes that defi ne virtual methods are extensible : You can add capabilities to a derived class without having access to the ancestor’s source statements.

Note:

Key points about virtual methods

A virtual method is one that a derived class can override.

You must implement a class’s virtual methods. (Pure virtual methods are not included in this requirement.)

A derived class does not need to override an existing implementation of an inherited virtual method.

Any of a class’s methods may be virtual. However, if you do not want a derived class to override a particular method, the method should not be virtual.

Constructors cannot be virtual.

Destructors can and should be virtual. Virtual destructors ensure that future descend- ants of the object can deallocate themselves correctly.

A virtual method’s return type cannot be overridden.

An ordinary C++ array is statically allocated

C2.5

Dynamic Allocation of Arrays

When you declare an array in C++ by using statements such as const int MAX_SIZE = 50;

double myArray[MAX_SIZE];

the compiler reserves a specifi c number— MAX_SIZE , in this case—of memory cells for the array. This memory allocation occurs before your program executes, so it is not possible to wait until execution to give MAX_SIZE a value. We have already discussed the problem this fi xed-size data structure causes when your program has more than MAX_SIZE items to place into the array.

Dynamic Allocation of Arrays 131

You just learned how to use the new operator to allocate memory dynamically—that is, during program execution. Although Section C2.3 showed you how to allocate memory for a single variable or object, you actually can allocate memory for many at one time. If you write

int arraySize = 50;

double* anArray = new double[arraySize];

the pointer variable anArray will point to the fi rst item in an array of 50 items. Unlike MAX_SIZE , arraySize can change during program execution. You can assign a value to arraySize at execution time and thus determine how large your array is. That is good, but how do you use this array?

Regardless of how you allocate an array—statically, as in the fi rst example, or dynamically, as in the second—you can use an index and the familiar array notation to access its elements. For example, anArray[0] and anArray[1] are the fi rst two items in the array anArray .

When you allocate an array dynamically, you need to return its memory cells to the system when you no longer need them. As described earlier, you use the delete operator to perform this task. To deallocate the array anArray , you write

delete [ ] anArray;

Note that you include brackets when you apply delete to an array.

Now suppose that your program uses all of the array anArray , despite having chosen its size dur- ing execution. You can allocate a new and larger array, copy the old array into the new array, and fi nally deallocate the old array. Doubling the size of the array each time it becomes full is a reasonable approach. The following statements double the size of anArray :

double* oldArray = anArray; // Copy pointer to array

anArray =new double[2 * arraySize]; // Double array size

for ( int index = 0; index < arraySize; index++) // Copy old array anArray[index] = oldArray[index];

delete [ ] oldArray; // Deallocate old array

Subsequent discussions in this book will refer to both statically allocated and dynamically allo- cated arrays. Our array-based ADT implementations will use statically allocated arrays for simplic- ity. The programming problems will ask you to create array-based implementations that use dynamically allocated arrays. We will refer to such arrays as resizable .