• No results found

C2.1 Memory Allocation for Variables and Early Binding of Methods

When you declare an ordinary variable x to have the data type int , the C++ compiler allocates a memory cell that can hold an integer. You use the identifi er x to refer to this cell. To put the value 5 in the cell, you could write

int x = 5;

To display the value that is in the cell, you could write cout << "The value of x is " << x << endl;

As discussed in Chapter 2 , a function’s locally declared variables such as x are placed into an activation record with its parameters and some bookkeeping data. These activation records are stored in an area of your application’s memory called the run-time stack . Each time a function is called, an activation record is automatically created on the run-time stack. When the function ends, the activation record is destroyed, freeing the memory used for the local variables and parameters. At that point, the function’s local variables and their values are no longer accessible to your program. Your program then returns to where the function was invoked and executes the statement following the function call. This behavior is the same for methods.

When you create an object, the storage for the data members of that object are also placed into an activation record for the currently executing function or method. The statements

PlainBox<string> myPlainBox;

MagicBox<string> myMagicBox = MagicBox<string>();

show two different ways of invoking default constructors to create instances of PlainBox and MagicBox , respectively. When these objects are instantiated, their data fi elds are placed on the run- time stack just as primitive data types are. Recall from C++ Interlude 1 that the names of those data fi elds are item and firstItemStored .

The compiler also knows that if you invoke the setItem method on these objects by writing myPlainBox.setItem("Fun Item");

myMagicBox.setItem("Secret Item");

the PlainBox version of the setItem method should be called for myPlainBox , and the MagicBox ver- sion of thesetItem method should be called for myMagicBox . This choice, which is an example of

early binding , is made during compilation and cannot be altered during execution.

Most of the time, this automatic memory management and early binding are all you need in your program. However, two situations can arise when they are not:

You want to take advantage of polymorphism.

You must access an object outside of the function or method that creates it.

C2.2

A Problem to Solve

Suppose your friend was so impressed with your work on the three boxes discussed in C++ Interlude 1 that she asked you to continue helping with the video game. She would like you to write a function that takes two arguments: an object of any of the three types of boxes and an item of type string . The function should place the item in the box by invoking the box’s setItem method.

Two situations when automatic memory management and early binding are insuffi cient

A Problem to Solve 119

Since ToyBox and MagicBox are derived from PlainBox , as Figure C2-1 illustrates, you may think that the following function defi nition would suffi ce:

void placeInBox(PlainBox<string>& theBox, string theItem) {

theBox.setItem(theItem); } // end placeInBox

The parameter theBox can accept as an argument any PlainBox object or an object of a class derived from PlainBox . This function could then be used in the following sequence of statements:

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

PlainBox<string> myPlainBox;

placeInBox(myPlainBox, specialItem); MagicBox<string> myMagicBox;

placeInBox(myMagicBox, otherItem);

placeInBox(myMagicBox, specialItem); // specialItem is stored! cout << myMagicBox.getItem() << endl; // "Riches beyond compare!"

Although this code compiles, it does not perform as you would expect. Since otherItem has already been stored in the magic box, specialItem should not replace that item. Unfortunately, when the item stored in the magic box is displayed, the output is Riches beyond compare. The reason is that, in our function, the statement

theBox.setItem(theItem);

invokes the PlainBox version of the setItem method instead of the MagicBox version. In this case, the compiler determined the version of the method to invoke from the type of the parameter theBox instead of from the type of its corresponding argument. The following set of statements has a similar result:

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

An example of early binding

FIGURE C2-1 UML class diagram for a family of classes BoxInterface

PlainBox

ToyBox MagicBox

Another example of early binding

PlainBox<string> mySpecialBox = MagicBox<string>(); mySpecialBox.setItem(otherItem);

mySpecialBox.setItem(specialItem); // specialItem is stored! cout << mySpecialBox.getItem() << endl; // "Riches beyond compare!"

In both situations, the version of setItem that will be called is determined when the program is compiled. In this second case, even though we instantiated a MagicBox object for mySpecialBox , the variable mySpecialBox is of type PlainBox , so the PlainBox version of setItem is called. The same decision logic applies to our function placeInBox .

This code is correct from the compiler’s perspective. The compiler assumes it is our intent to have the parameter theBox and the variable mySpecialBox behave as PlainBox objects. We need a way to communicate to the compiler that the code to execute should not be determined until the pro- gram is running. This is called late binding , which is an aspect of polymorphism. To solve this prob- lem and have both our function and simple-code examples execute as we intend, we need two tools: pointer variables and virtual methods.

C2.3

Pointers and the Program’s Free Store

To take advantage of late binding, we do not want our objects to be in an activation record on the run- time stack. We need another location. When a C++ program begins execution, in addition to the run- time stack, the operating system sets aside memory for the code—called code storage or text

storage —and for any global variables and static variables—called static storage . Your program is

also given extra memory, called the heap , or free store , which a programmer can use to store data. Figure C2-2 illustrates these portions of memory.

We allocate memory for a variable on the free store by using the new operator . After allocating memory for the variable, the new operator returns the memory address of the variable in the free store so the program can use it. This memory address must be placed in a special type of variable called a

pointer variable , or simply a pointer . A pointer variable contains the location, or address in mem-

ory, of a memory cell.

FIGURE C2-2 Sample program memory layout Run-time stack

Free store (Heap)

Static storage Code storage The new operator

allocates memory on the free store

VideoNote

C++ memory allocation

Pointers and the Program’s Free Store 121

Unlike variables on the run-time stack, which have their memory allocated and deallocated auto- matically, variables placed in the free store persist in memory even when the function or method that created them ends. This means that when a programmer allocates memory from the free store for a variable, the programmer has the responsibility to deallocate that memory when the variable is no longer needed. Failure to do so often results in a memory leak . A memory leak is memory that has been allocated for use but is no longer needed and cannot be accessed or deallocated.

To indicate that a variable is a pointer, we place the character * after the type of the data the pointer references. 1 For example, the statement

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

creates a MagicBox object in the free store and places the address of the object in the local variable myBoxPtr . We say that myBoxPtr points to a MagicBox object. To call a method of an object that is in the free store, we use the notation -> :

string someItem = "Something Free"; myBoxPtr->setItem(someItem);

Figure C2-3 shows the state of memory and the local variables after the previous statements execute.

Observe that this newly created object has no programmer-defi ned name. The only way to access its methods is indirectly via the pointer that new creates, that is, by using myBoxPtr-> as in the previ- ous example. Usually we will simplify the diagrams so that only the object in the free store and its pointer are drawn, as in Figure C2-4 .

If you allocate memory on the free store, you eventually must deallocate it

1 Some programmers place the * next to the name of the variable. The authors of this textbook believe placing the * next to the data type more clearly shows that the variable is of a pointer type.

Indicate a pointer type by writing an asterisk after the data type

FIGURE C2-3 Run-time stack and free store after myboxPtr points to a MagicBox object and its data memberitem is set

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

someItem myBoxPtr "Something" item "Something" MagicBoxobject Activation record

Free store (heap) Run-time stack

Creates variables on the run-time stack

string someItem = "Something";

myBoxPtr->setItem(someItem);

Creates an object in the free store

Pointer variables are simply another type of variable and follow the same rules as other variable types. For example, if you declare a pointer variable that points to a ToyBox<string> object, you can have it point only to ToyBox<string> objects, not MagicBox<string> objects or ToyBox<double> objects, as the following statements illustrate:

ToyBox<string>* toyPtr = new ToyBox <string>(); // OK ToyBox<string>* boxPtr = new MagicBox<string>(); // Error! ToyBox<string>* somePtr = new ToyBox <double>(); // Error!

If you have two pointer variables that can point to the same type of object, you can make them point to the same object by using the assignment operator =. For example, the statement

somePtr = toyPtr;

makes the variable somePtr point to the same ToyBox object to which toyPtr points. In such cases, it is helpful to think of the assignment operator as copying the value stored within the pointer variable on its right into the pointer variable on its left. Since pointers store addresses of objects, the value copied is the location of an object in the free store. The object itself is not copied. The result is that both pointer variables point to the same object, as shown in Figure C2-5 .

If you declare a pointer variable but do not immediately create an object for it to reference, you should set the pointer tonullptr . An assignment like the following one is necessary because C++ does not initialize the pointer for you:

ToyBox<int>* myToyPtr = nullptr;

FIGURE C2-4 myBoxPtr and the object to which it points

myBoxPtr

Object

FIGURE C2-5 Two pointer variables that point to the same object toyPtr

Object somePtr