Day 8 |
Figure 8.1. A schematic representation of theAge.
Different computers number this memory using different, complex schemes. Usually programmers don't need to know the particular address of any given variable, because the compiler handles the details. If you want this information, though, you can use the address of operator (&), which is illustrated in Listing 8.1.
Listing 8.1. Demonstrating address of variables.
1: // Listing 8.1 Demonstrates address of operator 2: // and addresses of local variables 3: 4: #include <iostream.h> 5: 6: int main() 7: { 8: unsigned short shortVar=5; 9: unsigned long longVar=65535; 10: long sVar = -65535; 11: 12: cout << "shortVar:\t" << shortVar; 13: cout << " Address of shortVar:\t"; 14: cout << &shortVar _<< "\n"; 15: 16: cout << "longVar:\t" << longVar; 17: cout << " Address of longVar:\t" ; 18: cout << &longVar _<< "\n"; 19: 20: cout << "sVar:\t" << sVar; 21: cout << " Address of sVar:\t" ; 22: cout << &sVar _<< "\n"; 23: 24: return 0; 25: } Output: shortVar: 5 Address of shortVar: 0x8fc9:fff4 longVar: 65535 Address of longVar: 0x8fc9:fff2 sVar: -65535 Address of sVar: 0x8fc9:ffee(Your printout may look different.)
Analysis: Three variables are declared
and initialized: a short in line 8, an unsigned long
in line 9, and a long in line 10. Their values and addresses are
printed in lines 12-16, by using the address of operator (&).
The value of shortVar is 5, as expected, and its
address is 0x8fc9:fff4 when run on my 80386-based computer. This
complicated address is computer-specific and may change slightly each time
the program is run. Your results will be different. What doesn't change,
however, is that the difference in the first two addresses is two bytes
if your computer uses two-byte short integers. The difference
between the second and third is four bytes if your computer uses four-byte
long integers. Figure 8.2 illustrates how the variables in this
program would be stored in memory.
Figure 8.2. Illustration of variable storage.
There is no reason why you need to know the actual numeric value of the address of each variable. What you care about is that each one has an address and that the right amount of memory is set aside. You tell the compiler how much memory to allow for your variables by declaring the variable type; the compiler automatically assigns an address for it. For example, a long integer is typically four bytes, meaning that the variable has an address to four bytes of memory.
For example, suppose that howOld is an integer. To declare a pointer called pAge to hold its address, you would write
int *pAge = 0;This declares pAge to be a pointer to int. That is, pAge is declared to hold the address of an int.
Note that pAge is a variable like any of the variables. When you declare an integer variable (type int), it is set up to hold an integer. When you declare a pointer variable like pAge, it is set up to hold an address. pAge is just a different type of variable.
In this example, pAge is initialized to zero. A pointer whose value is zero is called a null pointer. All pointers, when they are created, should be initialized to something. If you don't know what you want to assign to the pointer, assign 0. A pointer that is not initialized is called a wild pointer. Wild pointers are very dangerous.
If you do initialize the pointer to 0, you must specifically assign the address of howOld to pAge. Here's an example that shows how to do that:
NOTE: Practice safe computing: Initialize your pointers!
unsigned short int howOld = 50; // make a variable unsigned short int * pAge = 0; // make a pointer pAge = &howOld; // put howOld's address in pAgeThe first line creates a variable--howOld, whose type is unsigned short int--and initializes it with the value 50. The second line declares pAge to be a pointer to type unsigned short int and initializes it to zero. You know that pAge is a pointer because of the asterisk (*) after the variable type and before the variable name.
The third and final line assigns the address of howOld to the pointer pAge. You can tell that the address of howOld is being assigned because of the address of operator (&). If the address of operator had not been used, the value of howOld would have been assigned. That might, or might not, have been a valid address.
At this point, pAge has as its value the address of howOld. howOld, in turn, has the value 50. You could have accomplished this with one less step, as in
unsigned short int howOld = 50; // make a variable unsigned short int * pAge = &howOld; // make pointer to howOldpAge is a pointer that now contains the address of the howOld variable. Using pAge, you can actually determine the value of howOld, which in this case is 50. Accessing howOld by using the pointer pAge is called indirection because you are indirectly accessing howOld by means of pAge. Later today you will see how to use indirection to access a variable's value.
Normal variables provide direct access to their own values. If you create a new variable of type unsigned short int called yourAge, and you want to assign the value in howOld to that new variable, you would write
unsigned short int yourAge; yourAge = howOld;A pointer provides indirect access to the value of the variable whose address it stores. To assign the value in howOld to the new variable yourAge by way of the pointer pAge, you would write
unsigned short int yourAge; yourAge = *pAge;The indirection operator (*) in front of the variable pAge means "the value stored at." This assignment says, "Take the value stored at the address in pAge and assign it to yourAge."
NOTE: The indirection operator (*) is used in two distinct ways with pointers: declaration and dereference. When a pointer is declared, the star indicates that it is a pointer, not a normal variable. For example,unsigned short * pAge = 0; // make a pointer to an unsigned short
When the pointer is dereferenced, the indirection operator indicates that the value at the memory location stored in the pointer is to be accessed, rather than the address itself.*pAge = 5; // assign 5 to the value at pAge
Also note that this same character (*) is used as the multiplication operator. The compiler knows which operator to call, based on context.
Consider the following code fragment:
int theVariable = 5; int * pPointer = &theVariable ;theVariable is declared to be an integer variable initialized with the value 5. pPointer is declared to be a pointer to an integer; it is initialized with the address of theVariable. pPointer is the pointer. The address that pPointer holds is the address of theVariable. The value at the address that pPointer holds is 5. Figure 8.3 shows a schematic representation of theVariable and pPointer.
Figure 8.3. A schematic representation of memory.
Listing 8.2. Manipulating data by using pointers.
1: // Listing 8.2 Using pointers 2: 3: #include <iostream.h> 4: 5: typedef unsigned short int USHORT; 6: int main() 7: { 8: USHORT myAge; // a variable 9: USHORT * pAge = 0; // a pointer 10: myAge = 5; 11: cout << "myAge: " << myAge << "\n"; 12: 13: pAge = &myAge; // assign address of myAge to pAge 14: 15: cout << "*pAge: " << *pAge << "\n\n"; 16: 17: cout << "*pAge = 7\n"; 18: 19: *pAge = 7; // sets myAge to 7 20: 21: cout << "*pAge: " << *pAge << "\n"; 22: cout << "myAge: " << myAge << "\n\n"; 23: 24: 25: cout << "myAge = 9\n"; 26: 27: myAge = 9; 28: 29: cout << "myAge: " << myAge << "\n"; 30: cout << "*pAge: " << *pAge << "\n"; 31: 32: return 0; 33: } Output: myAge: 5 *pAge: 5 *pAge = 7 *pAge: 7 myAge: 7 myAge = 9 myAge: 9 *pAge: 9Analysis: This program declares two variables: an unsigned short, myAge, and a pointer to an unsigned short, pAge. myAge is assigned the value 5 on line 10; this is verified by the printout in line 11.
In line 27, the value 9 is assigned to the variable myAge. This value is obtained directly in line 29 and indirectly (by dereferencing pAge) in line 30.
Listing 8.3. Finding out what is stored in pointers.
1: // Listing 8.3 What is stored in a pointer. 2: 3: #include <iostream.h> 4: 5: typedef unsigned short int USHORT; 6: int main() 7: { 8: unsigned short int myAge = 5, yourAge = 10; 9: unsigned short int * pAge = &myAge; // a pointer 10: 11: cout << "myAge:\t" << myAge << "\tyourAge:\t" << yourAge << "\n"; 12: cout << "&myAge:\t" << &myAge << "\t&yourAge:\t" << &yourAge <<"\n"; 13: 14: cout << "pAge:\t" << pAge << "\n"; 15: cout << "*pAge:\t" << *pAge << "\n"; 16: 17: pAge = &yourAge; // reassign the pointer 18: 19: cout << "myAge:\t" << myAge << "\tyourAge:\t" << yourAge << "\n"; 20: cout << "&myAge:\t" << &myAge << "\t&yourAge:\t" << &yourAge <<"\n"; 21: 22: cout << "pAge:\t" << pAge << "\n"; 23: cout << "*pAge:\t" << *pAge << "\n"; 24: 25: cout << "&pAge:\t" << &pAge << "\n"; 26: return 0; 27: } Output: myAge: 5 yourAge: 10 &myAge: 0x355C &yourAge: 0x355E pAge: 0x355C *pAge: 5 myAge: 5 yourAge: 10 &myAge: 0x355C &yourAge: 0x355E pAge: 0x355E *pAge: 10 &pAge: 0x355A(Your output may look different.)
Analysis: In line 8, myAge
and yourAge are declared to be variables of type unsigned
short integer. In line 9, pAge is declared to be a pointer
to an unsigned short integer, and it is initialized with the address
of the variable myAge.
Lines 11 and 12 print the values and the addresses of myAge
and yourAge. Line 14 prints the contents of pAge, which
is the address of myAge. Line 15 prints the result of dereferencing
pAge, which prints the value at pAge--the value in myAge,
or 5.
This is the essence of pointers. Line 14 shows that pAge stores the address of myAge, and line 15 shows how to get the value stored in myAge by dereferencing the pointer pAge. Make sure that you understand this fully before you go on. Study the code and look at the output.
In line 17, pAge is reassigned to point to the address of yourAge. The values and addresses are printed again. The output shows that pAge now has the address of the variable yourAge and that dereferencing obtains the value in yourAge.
Line 25 prints the address of pAge itself. Like any variable, it has an address, and that address can be stored in a pointer. (Assigning the address of a pointer to another pointer will be discussed shortly.)
DO use the indirection operator (*) to access the data stored at the address in a pointer. DO initialize all pointers either to a valid address or to null (0). DO remember the difference between the address in a pointer and the value at that address.
unsigned short int * pPointer = 0;To assign or initialize a pointer, prepend the name of the variable whose address is being assigned with the address of operator (&). For example,
unsigned short int theVariable = 5; unsigned short int * pPointer = & theVariable;To dereference a pointer, prepend the pointer name with the dereference operator (*). For example,
unsigned short int theValue = *pPointer
The problem with local variables is that they don't persist: When the function returns, the local variables are thrown away. Global variables solve that problem at the cost of unrestricted access throughout the program, which leads to the creation of code that is difficult to understand and maintain. Putting data in the free store solves both of these problems.
You can think of the free store as a massive section of memory in which thousands of sequentially numbered cubbyholes lie waiting for your data. You can't label these cubbyholes, though, as you can with the stack. You must ask for the address of the cubbyhole that you reserve and then stash that address away in a pointer.
One way to think about this is with an analogy: A friend gives you the 800 number for Acme Mail Order. You go home and program your telephone with that number, and then you throw away the piece of paper with the number on it. If you push the button, a telephone rings somewhere, and Acme Mail Order answers. You don't remember the number, and you don't know where the other telephone is located, but the button gives you access to Acme Mail Order. Acme Mail Order is your data on the free store. You don't know where it is, but you know how to get to it. You access it by using its address--in this case, the telephone number. You don't have to know that number; you just have to put it into a pointer (the button). The pointer gives you access to your data without bothering you with the details.
The stack is cleaned automatically when a function returns. All the local variables go out of scope, and they are removed from the stack. The free store is not cleaned until your program ends, and it is your responsibility to free any memory that you've reserved when you are done with it.
The advantage to the free store is that the memory you reserve remains available until you explicitly free it. If you reserve memory on the free store while in a function, the memory is still available when the function returns.
The advantage of accessing memory in this way, rather than using global variables, is that only functions with access to the pointer have access to the data. This provides a tightly controlled interface to that data, and it eliminates the problem of one function changing that data in unexpected and unanticipated ways.
For this to work, you must be able to create a pointer to an area on the free store and to pass that pointer among functions. The following sections describe how to do this.
The return value from new is a memory address. It must be assigned to a pointer. To create an unsigned short on the free store, you might write
unsigned short int * pPointer; pPointer = new unsigned short int;You can, of course, initialize the pointer at its creation with
unsigned short int * pPointer = new unsigned short int;In either case, pPointer now points to an unsigned short int on the free store. You can use this like any other pointer to a variable and assign a value into that area of memory by writing
*pPointer = 72;This means, "Put 72 at the value in pPointer," or "Assign the value 72 to the area on the free store to which pPointer points."
If new cannot create memory on the free store (memory is, after all, a limited resource) it returns the null pointer. You must check your pointer for null each time you request new memory.
WARNING: Each time you allocate memory using the new keyword, you must check to make sure the pointer is not null.
To restore the memory to the free store, you use the keyword delete. For example,
delete pPointer;When you delete the pointer, what you are really doing is freeing up the memory whose address is stored in the pointer. You are saying, "Return to the free store the memory that this pointer points to." The pointer is still a pointer, and it can be reassigned. Listing 8.4 demonstrates allocating a variable on the heap, using that variable, and deleting it.
Listing 8.4. Allocating, using, and deleting pointers.
WARNING: When you call delete on a pointer, the memory it points to is freed. Calling delete on that pointer again will crash your program! When you delete a pointer, set it to zero (null). Calling delete on a null pointer is guaranteed to be safe. For example:Animal *pDog = new Animal; delete pDog; //frees the memorypDog = 0; //sets pointer to null //... delete pDog; //harmless
1: // Listing 8.4 2: // Allocating and deleting a pointer 3: 4: #include <iostream.h> 5: int main() 6: { 7: int localVariable = 5; 8: int * pLocal= &localVariable; 9: int * pHeap = new int; 10: if (pHeap == NULL) 11: { 12: cout << "Error! No memory for pHeap!!"; 13: return 0; 14: } 15: *pHeap = 7; 16: cout << "localVariable: " << localVariable << "\n"; 17: cout << "*pLocal: " << *pLocal << "\n"; 18: cout << "*pHeap: " << *pHeap << "\n"; 19: delete pHeap; 20: pHeap = new int; 21: if (pHeap == NULL) 22: { 23: cout << "Error! No memory for pHeap!!"; 24: return 0; 25: } 26: *pHeap = 9; 27: cout << "*pHeap: " << *pHeap << "\n"; 28: delete pHeap; 29: return 0; 30: } Output: localVariable: 5 *pLocal: 5 *pHeap: 7 *pHeap: 9Analysis: Line 7 declares and initializes a local variable. Line 8 declares and initializes a pointer with the address of the local variable. Line 9 declares another pointer but initializes it with the result obtained from calling new int. This allocates space on the free store for an int. Line 10 verifies that memory was allocated and the pointer is valid (not null). If no memory can be allocated, the pointer is null and an error message is printed.
Line 15 assigns the value 7 to the newly allocated memory. Line 16 prints the value of the local variable, and line 17 prints the value pointed to by pLocal. As expected, these are the same. Line 19 prints the value pointed to by pHeap. It shows that the value assigned in line 15 is, in fact, accessible.
In line 19, the memory allocated in line 9 is returned to the free store by a call to delete. This frees the memory and disassociates the pointer from that memory. pHeap is now free to point to other memory. It is reassigned in lines 20 and 26, and line 27 prints the result. Line 28 restores that memory to the free store.
Although line 28 is redundant (the end of the program would have returned that memory) it is a good idea to free this memory explicitly. If the program changes or is extended, it will be beneficial that this step was already taken care of.
1: unsigned short int * pPointer = new unsigned short int; 2: *pPointer = 72; 3: pPointer = new unsigned short int; 4: *pPointer = 84;Line 1 creates pPointer and assigns it the address of an area on the free store. Line 2 stores the value 72 in that area of memory. Line 3 reassigns pPointer to another area of memory. Line 4 places the value 84 in that area. The original area--in which the value 72 is now held--is unavailable because the pointer to that area of memory has been reassigned. There is no way to access that original area of memory, nor is there any way to free it before the program ends.
The code should have been written like this:
1: unsigned short int * pPointer = new unsigned short int; 2: *pPointer = 72; 3: delete pPointer; 4: pPointer = new unsigned short int; 5: *pPointer = 84;Now the memory originally pointed to by pPointer is deleted, and thus freed, in line 3.
NOTE: For every time in your program that you call new, there should be a call to delete. It is important to keep track of which pointer owns an area of memory and to ensure that the memory is returned to the free store when you are done with it.
Cat *pCat = new Cat;This calls the default constructor--the constructor that takes no parameters. The constructor is called whenever an object is created (on the stack or on the free store).
Listing 8.5. Creating and deleting objects on the free store.
1: // Listing 8.5 2: // Creating objects on the free store 3: 4: #include <iostream.h> 5: 6: class SimpleCat 7: { 8: public: 9: SimpleCat(); 10: ~SimpleCat(); 11 private: 12 int itsAge; 13 }; 14 15 SimpleCat::SimpleCat() 16 { 17 cout << "Constructor called.\n"; 18 itsAge = 1; 19 } 20 21 SimpleCat::~SimpleCat() 22 { 23 cout << "Destructor called.\n"; 24 } 25 26 int main() 27 { 28 cout << "SimpleCat Frisky...\n"; 29 SimpleCat Frisky; 30 cout << "SimpleCat *pRags = new SimpleCat...\n"; 31 SimpleCat * pRags = new SimpleCat; 32 cout << "delete pRags...\n"; 33 delete pRags; 34 cout << "Exiting, watch Frisky go...\n"; 35 return 0; 36 } Output: SimpleCat Frisky... Constructor called. SimpleCat *pRags = new SimpleCat.. Constructor called. delete pRags... Destructor called. Exiting, watch Frisky go... Destructor called.Analysis: Lines 6-13 declare the stripped-down class SimpleCat. Line 9 declares SimpleCat's constructor, and lines 15-19 contain its definition. Line 10 declares SimpleCat's destructor, and lines 21-24 contain its definition.
(*pRags).GetAge();Parentheses are used to assure that pRags is dereferenced before GetAge() is accessed.
Because this is cumbersome, C++ provides a shorthand operator for indirect access: the points-to operator (->), which is created by typing the dash (-) immediately followed by the greater-than symbol (>). C++ treats this as a single symbol. Listing 8.6 demonstrates accessing member variables and functions of objects created on the free store.
Listing 8.6. Accessing member data of objects on the free store.
1: // Listing 8.6 2: // Accessing data members of objects on the heap 3: 4: #include <iostream.h> 5: 6: class SimpleCat 7: { 8: public: 9: SimpleCat() {itsAge = 2; } 10: ~SimpleCat() {} 11: int GetAge() const { return itsAge; } 12: void SetAge(int age) { itsAge = age; } 13: private: 14: int itsAge; 15: }; 16: 17: int main() 18: { 19: SimpleCat * Frisky = new SimpleCat; 20: cout << "Frisky is " << Frisky->GetAge() << " years old\n"; 21: Frisky->SetAge(5); 22: cout << "Frisky is " << Frisky->GetAge() << " years old\n"; 23: delete Frisky; 24: return 0; 25: } Output: Frisky is 2 years old Frisky is 5 years oldAnalysis: In line 19, a SimpleCat object is instantiated on the free store. The default constructor sets its age to 2, and the GetAge() method is called in line 20. Because this is a pointer, the indirection operator (->) is used to access the member data and functions. In line 21, the SetAge() method is called, and GetAge() is accessed again in line 22.
Listing 8.7. Pointers as member data.
1: // Listing 8.7 2: // Pointers as data members 3: 4: #include <iostream.h> 5: 6: class SimpleCat 7: { 8: public: 9: SimpleCat(); 10: ~SimpleCat(); 11: int GetAge() const { return *itsAge; } 12: void SetAge(int age) { *itsAge = age; } 13: 14: int GetWeight() const { return *itsWeight; } 15: void setWeight (int weight) { *itsWeight = weight; } 16: 17: private: 18: int * itsAge; 19: int * itsWeight; 20: }; 21: 22: SimpleCat::SimpleCat() 23: { 24: itsAge = new int(2); 25: itsWeight = new int(5); 26: } 27: 28: SimpleCat::~SimpleCat() 29: { 30: delete itsAge; 31: delete itsWeight; 32: } 33: 34: int main() 35: { 36: SimpleCat *Frisky = new SimpleCat; 37: cout << "Frisky is " << Frisky->GetAge() << " years old\n"; 38: Frisky->SetAge(5); 39: cout << "Frisky is " << Frisky->GetAge() << " years old\n"; 40: delete Frisky; 41: return 0; 42: } Output: Frisky is 2 years old Frisky is 5 years oldAnalysis: The class SimpleCat is declared to have two member variables--both of which are pointers to integers--on lines 14 and 15. The constructor (lines 22-26) initializes the pointers to memory on the free store and to the default values.
The calling function (in this case, main()) is unaware that itsAge and itsWeight are point-ers to memory on the free store. main() continues to call GetAge() and SetAge(), and the details of the memory management are hidden in the implementation of the class--as they should be.
When Frisky is deleted in line 40, its destructor is called. The destructor deletes each of its member pointers. If these, in turn, point to objects of other user-defined classes, their destructors are called as well.
It is possible to use the this pointer explicitly, as Listing 8.8 illustrates.
Listing 8.8. Using the this pointer.
1: // Listing 8.8 2: // Using the this pointer 3: 4: #include <iostream.h> 5: 6: class Rectangle 7: { 8: public: 9: Rectangle(); 10: ~Rectangle(); 11: void SetLength(int length) { this->itsLength = length; } 12: int GetLength() const { return this->itsLength; } 13: 14: void SetWidth(int width) { itsWidth = width; } 15: int GetWidth() const { return itsWidth; } 16: 17: private: 18: int itsLength; 19: int itsWidth; 20: }; 21: 22: Rectangle::Rectangle() 23: { 24: itsWidth = 5; 25: itsLength = 10; 26: } 27: Rectangle::~Rectangle() 28: {} 29: 30: int main() 31: { 32: Rectangle theRect; 33: cout << "theRect is " << theRect.GetLength() << " feet long.\n"; 34: cout << "theRect is " << theRect.GetWidth() << " feet wide.\n"; 35: theRect.SetLength(20); 36: theRect.SetWidth(10); 37: cout << "theRect is " << theRect.GetLength()<< " feet long.\n"; 38: cout << "theRect is " << theRect.GetWidth()<< " feet wide.\n"; 39: return 0; 40: } Output: theRect is 10 feet long. theRect is 5 feet long. theRect is 20 feet long. theRect is 10 feet long.Analysis: The SetLength() and GetLength() accessor functions explicitly use the this pointer to access the member variables of the Rectangle object. The SetWidth and GetWidth accessors do not. There is no difference in their behavior, although the syntax is easier to understand.
You'll see a practical use for the this pointer on Day 10, "Advanced Functions," when operator overloading is discussed. For now, your goal is to know about the this pointer and to understand what it is: a pointer to the object itself.
You don't have to worry about creating or deleting the this pointer. The compiler takes care of that.
It is as though the Acme Mail Order company moved away, and you still pressed the programmed button on your phone. It is possible that nothing terrible happens--a telephone rings in a deserted warehouse. Perhaps the telephone number has been reassigned to a munitions factory, and your call detonates an explosive and blows up your whole city!
In short, be careful not to use a pointer after you have called delete on it. The pointer still points to the old area of memory, but the compiler is free to put other data there; using the pointer can cause your program to crash. Worse, your program might proceed merrily on its way and crash several minutes later. This is called a time bomb, and it is no fun. To be safe, after you delete a pointer, set it to null (0). This disarms the pointer.
Listing 8.9 illustrates creating a stray pointer.
NOTE: Stray pointers are often called wild pointers or dangling pointers.
Listing 8.9. Creating a stray pointer.
WARNING: This program intentionally creates a stray pointer. Do NOT run this program--it will crash, if you are lucky.
1: // Listing 8.9 2: // Demonstrates a stray pointer 3: typedef unsigned short int USHORT; 4: #include <iostream.h> 5: 6: int main() 7: { 8: USHORT * pInt = new USHORT; 9: *pInt = 10; 10: cout << "*pInt: " << *pInt << endl; 11: delete pInt; 12: pInt = 0; 13: long * pLong = new long; 14: *pLong = 90000; 15: cout << "*pLong: " << *pLong << endl; 16: 17: *pInt = 20; // uh oh, this was deleted! 18: 19: cout << "*pInt: " << *pInt << endl; 20: cout << "*pLong: " << *pLong << endl; 21: delete pLong; 22: return 0; 23: } Output: *pInt: 10 *pLong: 90000 *pInt: 20 *pLong: 65556 Null pointer assignment(Your output may look different.)
Analysis: Line 8 declares pInt
to be a pointer to USHORT, and pInt is pointed to newly
allocated memory. Line 9 puts the value 10 in that memory, and
line 10 prints its value. After the value is printed, delete is
called on the pointer. pInt is now a stray, or dangling, pointer.
Line 13 declares a new pointer, pLong, which is pointed at
the memory allocated by new.
Line 14 assigns the value 90000 to pLong, and line 15 prints its value.
Line 17 assigns the value 20 to the memory that pInt points to, but pInt no longer points anywhere that is valid. The memory that pInt points to was freed by the call to delete, so assigning a value to that memory is certain disaster.
Line 19 prints the value at pInt. Sure enough, it is 20. Line 20 prints 20, the value at pLong; it has suddenly been changed to 65556. Two questions arise:
1. How could pLong's value change, given that pLong wasn't touched?
2. Where did the 20 go when pInt was used in line 17?
As you might guess, these are related questions. When a value was placed
at pInt in line 17, the compiler happily placed the value 20
at the memory location that pInt previously pointed to. However,
because that memory was freed in line 11, the compiler was free to reassign
it. When pLong was created in line 13, it was given pInt's
old memory location. (On some computers this may not happen, depending
on where in memory these values are stored.) When the value 20
was assigned to the location that pInt previously pointed to,
it wrote over the value pointed to by pLong. This is called "stomping
on a pointer." It is often the unfortunate outcome of using a stray pointer.
This is a particularly nasty bug, because the value that changed wasn't
associated with the stray pointer. The change to the value at pLong
was a side effect of the misuse of pInt. In a large program, this
would be very difficult to track down.
Just for fun, here are the details of how 65,556 got into that memory address:
2. delete was called on pInt, which told
the compiler that it could put something else at that location. Then pLong
was assigned the same memory location.
3. The value 90000 was assigned to *pLong. The particular computer used in this example stored the four-byte value of 90,000 (00 01 5F 90) in byte-swapped order. Therefore, it was stored as 5F 90 00 01.
4. pInt was assigned the value 20--or 00 14 in hexadecimal notation. Because pInt still pointed to the same address, the first two bytes of pLong were overwritten, leaving 00 14 00 01.
5. The value at pLong was printed, reversing the bytes back to their correct order of 00 01 00 14, which was translated into the DOS value of 65556.
const int * pOne; int * const pTwo; const int * const pThree;pOne is a pointer to a constant integer. The value that is pointed to can't be changed.
pTwo is a constant pointer to an integer. The integer can be changed, but pTwo can't point to anything else.
pThree is a constant pointer to a constant integer. The value that is pointed to can't be changed, and pThree can't be changed to point to anything else.
The trick to keeping this straight is to look to the right of the keyword const to find out what is being declared constant. If the type is to the right of the keyword, it is the value that is constant. If the variable is to the right of the keyword const, it is the pointer variable itself that is constant.
const int * p1; // the int pointed to is constant int * const p2; // p2 is constant, it can't point to anything else
If you declare a pointer to a const object, the only methods that you can call with that pointer are const methods. Listing 8.10 illustrates this.
Listing 8.10. Using pointers to const objects.
1: // Listing 8.10 2: // Using pointers with const methods 3: 4: #include <iostream.h> 5: 6: class Rectangle 7: { 8: public: 9: Rectangle(); 10: ~Rectangle(); 11: void SetLength(int length) { itsLength = length; } 12: int GetLength() const { return itsLength; } 13: 14: void SetWidth(int width) { itsWidth = width; } 15: int GetWidth() const { return itsWidth; } 16: 17: private: 18: int itsLength; 19: int itsWidth; 20: }; 21: 22: Rectangle::Rectangle(): 23: itsWidth(5), 24: itsLength(10) 25: {} 26: 27: Rectangle::~Rectangle() 28: {} 29: 30: int main() 31: { 32: Rectangle* pRect = new Rectangle; 33: const Rectangle * pConstRect = new Rectangle; 34: Rectangle * const pConstPtr = new Rectangle; 35: 36: cout << "pRect width: " << pRect->GetWidth() << " feet\n"; 37: cout << "pConstRect width: " << pConstRect->GetWidth() << " feet\n"; 38: cout << "pConstPtr width: " << pConstPtr->GetWidth() << " feet\n"; 39: 40: pRect->SetWidth(10); 41: // pConstRect->SetWidth(10); 42: pConstPtr->SetWidth(10); 43: 44: cout << "pRect width: " << pRect->GetWidth() << " feet\n"; 45: cout << "pConstRect width: " << pConstRect->GetWidth() << " feet\n"; 46: cout << "pConstPtr width: " << pConstPtr->GetWidth() << " feet\n"; 47: return 0; 48: } Output: pRect width: 5 feet pConstRect width: 5 feet pConstPtr width: 5 feet pRect width: 10 feet pConstRect width: 5 feet pConstPtr width: 10 feetAnalysis: Lines 6-20 declare Rectangle. Line 15 declares the GetWidth() member method const. Line 32 declares a pointer to Rectangle. Line 33 declares pConstRect, which is a pointer to a constant Rectangle. Line 34 declares pConstPtr, which is a constant pointer to Rectangle.
In line 40, pRect is used to set the width of the rectangle to 10. In line 41, pConstRect would be used, but it was declared to point to a constant Rectangle. Therefore, it cannot legally call a non-const member function; it is commented out. In line 38, pConstPtr calls SetAge(). pConstPtr is declared to be a constant pointer to a rectangle. In other words, the pointer is constant and cannot point to anything else, but the rectangle is not constant.
Constant objects and constant pointers will be discussed again tomorrow, when references to constant objects are discussed.
DO protect objects passed by reference with const if they should not be changed. DO pass by reference when the object can be changed. DO pass by value when small objects should not be changed.
Pointers are declared by writing the type of object that they point to, followed by the indirection operator (*) and the name of the pointer. Pointers should be initialized to point to an object or to null (0).
You access the value at the address stored in a pointer by using the indirection operator (*). You can declare const pointers, which can't be reassigned to point to other objects, and pointers to const objects, which can't be used to change the objects to which they point.
To create new objects on the free store, you use the new keyword and assign the address that is returned to a pointer. You free that memory by calling the delete keyword on the pointer. delete frees the memory, but it doesn't destroy the pointer. Therefore, you must reassign the pointer after its memory has been freed.
A. Today you saw how pointers are used to hold the address
of objects on the free store and how they are used to pass arguments by
reference. In addition, on Day 13, "Polymorphism," you'll see how pointers
are used in class polymorphism.
Q. Why should I bother to declare anything on the free store?
A. Objects on the free store persist after the return of a function. Additionally, the ability to store objects on the free store enables you to decide at runtime how many objects you need, instead of having to declare this in advance. This is explored in greater depth tomorrow.
Q. Why should I declare an object const if it limits what I can do with it?
A. As a programmer, you want to enlist the compiler in helping you find bugs. One serious bug that is difficult to find is a function that changes an object in ways that aren't obvious to the calling function. Declaring an object const prevents such changes.
2. What operator is used to find the value stored at an address
held in a pointer?
3. What is a pointer?
4. What is the difference between the address stored in a pointer and the value at that address?
5. What is the difference between the indirection operator and the address of operator?
6. What is the difference between const int * ptrOne and int * const ptrTwo?
3. Assign the value 50 to the variable yourAge by using the pointer that you declared in Exercise 2.
4. Write a small program that declares an integer and a pointer to integer. Assign the address of the integer to the pointer. Use the pointer to set a value in the integer variable.
5. BUG BUSTERS: What is wrong with this code?
#include <iostream.h> int main() { int *pInt; *pInt = 9; cout << "The value at pInt: " << *pInt; return 0; }
int main() { int SomeVariable = 5; cout << "SomeVariable: " << SomeVariable << "\n"; int *pVar = & SomeVariable; pVar = 9; cout << "SomeVariable: " << *pVar << "\n"; return 0; }