One of the most powerful tools available to a C++ programmer is the ability to manipulate computer memory directly by using pointers. ToChapter you will learn
Pointers present two special challenges when learning C++: They can be somewhat confusing, and it isn't immediately obvious why they are needed. This chapter explains how pointers work, step by step. You will fully understand the need for pointers, however, only as the guide progresses.
To understand pointers, you must know a little about computer memory. Computer
memory is divided into sequentially numbered memory locations. Each variable is located
at a unique location in memory, known as its address. (This is discussed in the "Extra
Credit" section following Chapter 5, "Functions.") Figure 8.1 shows a
schematic representation of the storage of an unsigned long integer variable
theAge.
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.
Every variable has an address. Even without knowing the specific address of a given variable, you can store that address in a pointer.
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.
NOTE: Practice safe computing: Initialize your pointers!
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:
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 toChapter you will see how to use indirection to access a variable's value.
Pointers can have any name that is legal for other variables. This guide follows the convention of naming all pointers with an initial p, as in pAge or pNumber.
The indirection operator (*) is also called the dereference operator. When a pointer is dereferenced, the value at the address stored by the pointer is retrieved.
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.
It is important to distinguish between a pointer, the address that the pointer holds, and the value at the address held by the pointer. This is the source of much of the confusion about pointers.
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.
Once a pointer is assigned the address of a variable, you can use that pointer to access the data in that variable. Listing 8.2 demonstrates how the address of a local variable is assigned to a pointer and how the pointer manipulates the values in that variable.
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.
On line 13, pAge is assigned the address of myAge. On line 15,
pAge is dereferenced and printed, showing that the value at the address
that pAge stores is the 5 stored in myAge. In line 17,
the value 7 is assigned to the variable at the address stored in pAge.
This sets myAge to 7, and the printouts in lines 21-22 confirm
this.
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.
Pointers enable you to manipulate addresses without ever knowing their real value. After toChapter, you'll take it on faith that when you assign the address of a variable to a pointer, it really has the address of that variable as its value. But just this once, why not check to make sure? Listing 8.3 illustrates this idea.
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.
To declare a pointer, write the type of the variable or object whose address will be stored in the pointer, followed by the pointer operator (*) and the name of the pointer. For example,
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 = *pPointerSo far you've seen step-by-step details of assigning a variable's address to a pointer. In practice, though, you would never do this. After all, why bother with a pointer when you already have a variable with access to that value? The only reason for this kind of pointer manipulation of an automatic variable is to demonstrate how pointers work. Now that you are comfortable with the syntax of pointers, you can put them to good use. Pointers are used, most often, for three tasks:
This rest of this chapter focuses on managing data on the free store and accessing class member data and functions. Tomorrow you will learn about passing variables by reference.
In the "Extra Credit" section following the discussion of functions in Chapter 5, five areas of memory are mentioned:
Local variables are on the stack, along with function parameters. Code is in code space, of course, and global variables are in global name space. The registers are used for internal housekeeping functions, such as keeping track of the top of the stack and the instruction pointer. Just about all remaining memory is given over to the free store, which is sometimes referred to as the heap.
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.
You allocate memory on the free store in C++ by using the new keyword. new is followed by the type of the object that you want to allocate so that the compiler knows how much memory is required. Therefore, new unsigned short int allocates two bytes in the free store, and new long allocates four.
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.
When you are finished with your area of memory, you must call delete on the pointer. delete returns the memory to the free store. Remember that the pointer itself--as opposed to the memory to which it points--is a local variable. When the function in which it is declared returns, that pointer goes out of scope and is lost. The memory allocated with new is not freed automatically, however. That memory becomes unavailable--a situation called a memory leak. It's called a memory leak because that memory can't be recovered until the program ends. It is as though the memory has leaked out of your computer.
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.
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
Listing 8.4. Allocating, using, and deleting pointers.
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.
To keep things simple, this error checking often won't be reproduced in future programs,
but you must include some sort of error checking in your own programs.
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.
Another way you might inadvertently create a memory leak is by reassigning your pointer before deleting the memory to which it points. Consider this code fragment:
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.
Just as you can create a pointer to an integer, you can create a pointer to any object. If you have declared an object of type Cat, you can declare a pointer to that class and instantiate a Cat object on the free store, just as you can make one on the stack. The syntax is the same as for integers:
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).
When you call delete on a pointer to an object on the free store, that object's destructor is called before the memory is released. This gives your class a chance to clean up, just as it does for objects destroyed on the stack. Listing 8.5 illustrates creating and deleting objects 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.
In line 29, Frisky is created on the stack, which causes the constructor
to be called. In line 31, the SimpleCat pointed to by pRags is
created on the heap; the constructor is called again. In line 33, delete is called
on pRags, and the destructor is called. When the function ends, Frisky
goes out of scope, and the destructor is called.
You accessed data members and functions by using the dot (.) operator for Cat objects created locally. To access the Cat object on the free store, you must dereference the pointer and call the dot operator on the object pointed to by the pointer. Therefore, to access the GetAge member function, you would write
(*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.
One or more of the data members of a class can be a pointer to an object on the free store. The memory can be allocated in the class constructor or in one of its methods, and it can be deleted in its destructor, as Listing 8.7 illustrates.
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 destructor (lines 28-32) cleans up the allocated memory. Because this is the
destructor, there is no point in assigning these pointers to null, as they
will no longer be accessible. This is one of the safe places to break the rule that
deleted pointers should be assigned to null, although following the rule
doesn't hurt.
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.
Every class member function has a hidden parameter: the this pointer. this points to the individual object. Therefore, in each call to GetAge() or SetAge(), the this pointer for the object is included as a hidden parameter.
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.
If that were all there was to the this pointer, there would be little point
in bothering you with it. The this pointer, however, is a pointer; it stores
the memory address of an object. As such, it can be a powerful tool.
You'll see a practical use for the this pointer on Chapter 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.
One source of bugs that are nasty and difficult to find is stray pointers. A stray pointer is created when you call delete on a pointer--thereby freeing the memory that it points to--and later try to use that pointer again without reassigning it.
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.
NOTE: Stray pointers are often called wild pointers or dangling pointers.
Listing 8.9 illustrates creating a stray pointer.
WARNING: This program intentionally creates a stray pointer. Do NOT run this program--it will crash, if you are lucky.
Listing 8.9. Creating a stray pointer.
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:
You can use the keyword const for pointers before the type, after the type, or in both places. For example, all of the following are legal declarations:
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 elseOn Chapter 6, "Basic Classes," you learned that you can apply the keyword const to a member function. When a function is declared const, the compiler flags as an error any attempt to change data in the object from within that function.
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 feet
Analysis: 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.
Lines 36-38 print their values.
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.
When you declare an object to be const, you are in effect declaring that the this pointer is a pointer to a const object. A const this pointer can be used only with const mem- ber functions.
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 provide a powerful way to access data by indirection. Every variable has an address, which can be obtained using the address of operator (&). The address can be stored in a pointer.
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.
The Workshop provides quiz questions to help you solidify your understanding of the material covered and exercises to provide you with experience in using what you've learned. Try to answer the quiz and exercise questions before checking the answers in Appendix D, and make sure you understand the answers before continuing to the next chapter.