Day 8. Understanding Pointers

One of the powerful but low-level tools available to a C++ programmer is the capability to manipulate computer memory directly by using pointers. This is an advantage that C++ has over some other languages, such as C# and Visual Basic.

Today, you will learn

• What pointers are

• How to declare and use pointers

• What the free store is and how to manipulate memory

Pointers present two special challenges when you’re learning C++: They can be somewhat confusing, and it isn’t immediately obvious why they are needed. Today’s lesson explains how pointers work, step-by-step. You will fully understand the need for pointers, however, only as the book progresses.

Note

The ability to use pointers and manipulate memory at a low level is one of the factors that makes C++ the language of choice for embedded and real-time applications.

What Is a Pointer?

A pointer is a variable that holds a memory address. That’s it. If you understand this simple sentence, then you know the core of what there is to know about pointers.

A Bit About Memory

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. Figure 8.1 shows a schematic representation of the storage of an unsigned long integer variable named theAge.

Figure 8.1. A schematic representation of theAge.

Image

Getting a Variable’s Memory Address

Different computers number this memory using different complex schemes. Usually, as a programmer, you 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 returns the address of an object in memory. Listing 8.1 is used to illustrate the use of this operator.

Listing 8.1. Demonstrating the Address-of Operator


1:  // Listing 8.1 Demonstrates address-of operator
2:  // and addresses of local variables
3:  #include <iostream>
4:
5:  int main()
6:  {
7:     using namespace std;
8:     unsigned short shortVar=5;
9:     unsigned long  longVar=65535;
10:     long sVar = -65535;
11:
12:     cout << "shortVar: " << shortVar;
13:     cout << " Address of shortVar: ";
14:     cout <<  &shortVar  << endl;
15:
16:     cout << "longVar: "  << longVar;
17:     cout  << " Address of longVar: " ;
18:     cout <<  &longVar  << endl;
19:
20:     cout << "sVar: "     << sVar;
21:     cout << " Address of sVar: " ;
22:     cout <<  &sVar     << endl;
23:
24:     return 0;
25:  }

Image

Image

(Your printout might look different, especially the last column.)

Image

Three variables are declared and initialized: an unsigned short on line 8, an unsigned long on line 9, and a long on line 10. Their values and addresses are printed on lines 12–22. You can see on lines 14, 18, and 22 that the address-of operator (&) is used to get the address of the variable. This operator is simply placed on the front of the variable name in order to have the address returned.

Line 12 prints the value of shortVar as 5, which is expected. In the first line of the output, you can see that its address is 0012FF7C when run on a Pentium (32-bit) computer. This address is computer-specific and might change slightly each time the program is run. Your results will be different.

When you declare a variable, the compiler determines how much memory to allow based on the variable type. The compiler takes care of allocating memory and automatically assigns an address for it. For a long integer that is typically four bytes, for example, an address to four bytes of memory is used.

Note

Note that your compiler might insist on assigning new variables on four-byte boundaries. (Thus, longVar was assigned an address four bytes after shortVar even though shortVar only needed two bytes!)

Storing a Variable’s Address in a Pointer

Every variable has an address. Even without knowing the specific address, you can store a variable’s address in a pointer.

Suppose, for example, that howOld is an integer. To declare a pointer called pAge to hold its address, you write


int *pAge = 0;

This declares pAge to be a pointer to an int. That is, pAge is declared to hold the address of an integer.

Note that pAge is a variable. When you declare an integer variable (type int), the compiler sets aside enough memory to hold an integer. When you declare a pointer variable such as pAge, the compiler sets aside enough memory to hold an address (on most computers, four bytes). A pointer, and thus pAge, is just a different type of variable.

Pointer Names

Because pointers are just another variable, you can use any name that is legal for other variables. The same naming rules and suggestions apply. Many programmers follow the convention of naming all pointers with an initial p, as in pAge or pNumber.

In the example,


int *pAge = 0;

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 because you have no idea what it is pointing to—and it could be pointing to anything! Wild pointers are very dangerous.

Note

Practice safe computing: Initialize all of your pointers!

For a pointer to hold an address, the address must be assigned to it. For the previous example, you must specifically assign the address of howOld to pAge, as shown in the following example:

Image

The first line creates a variable named 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 fewer step, as in

Image

pAge is a pointer that now contains the address of the howOld variable.

Getting the Value from a Variable

Using pAge, you can actually determine the value of howOld, which in this case is 50. Accessing the value stored in a variable by using a pointer is called indirection because you are indirectly accessing the variable by means of the pointer. For example, you can use indirection with the pAge pointer to access the value in howOld.

Indirection means accessing the value at the address held by a pointer. The pointer provides an indirect way to get the value held at that address.

Note

With a normal variable, the type tells the compiler how much memory is needed to hold the value. With a pointer, the type does not do this; all pointers are the same size—usually four bytes on a machine with a 32-bit processor and eight bytes on a machine with a 64-bit processor.

The type tells the compiler how much memory is needed for the object at the address, which the pointer holds!

In the declaration

Image

pAge is declared to be a pointer to an unsigned short integer. This tells the compiler that the pointer (which needs four bytes to hold an address) will hold the address of an object of type unsigned short int, which itself requires two bytes.

Dereferencing with the Indirection Operator

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 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 write


unsigned short int yourAge;
yourAge = *pAge;

The indirection operator (*) in front of the pointer variable pAge means “the value stored at.” This assignment says, “Take the value stored at the address in pAge and assign it to yourAge.” If you didn’t include the indirection operator:


yourAge = pAge;  // bad!!

you would be attempting to assign the value in pAge, a memory address, to YourAge. Your compiler would most likely give you a warning that you are making a mistake.

Different Uses of the Asterisk

The asterisk (*) is used in two distinct ways with pointers: as part of the pointer declaration and also as the dereference operator.

When you declare a pointer, the * is part of the declaration and it follows the type of the object pointed to. For example,


// make a pointer to an unsigned short
unsigned short * pAge = 0;

When the pointer is dereferenced, the dereference (or indirection) operator indicates that the value at the memory location stored in the pointer is to be accessed, rather than the address itself.


// assign 5 to the value at pAge
*pAge = 5;

Also note that this same character (*) is used as the multiplication operator. The compiler knows which operator to call based on how you are using it (context).

Pointers, Addresses, and Variables

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.2 shows a schematic representation of theVariable and pPointer.

Figure 8.2. A schematic representation of memory.

Image

In Figure 8.2, the value 5 is stored at address location 101. This is shown in the binary number


0000 0000 0000 0101

This is two bytes (16 bits) whose decimal value is 5.

The pointer variable is at location 106. Its value is


000 0000 0000 0000 0000 0000 0110 0101

This is the binary representation of the value 101, which is the address of theVariable, whose value is 5.

The memory layout here is schematic, but it illustrates the idea of how pointers store an address.

Manipulating Data by Using Pointers

In addition to using the indirection operator to see what data is stored at a location pointed to by a variable, you can also manipulate that data. After the pointer is assigned the address, you can use that pointer to access the data in the variable being pointed to.

Listing 8.2 pulls together what you have just learned about pointers. In this listing, you see how the address of a local variable is assigned to a pointer and how the pointer can be used along with the indirection operator to manipulate the values in that variable.

Listing 8.2. Manipulating Data by Using Pointers

Image

Image


myAge: 5
*pAge: 5

Setting *pAge = 7...
*pAge: 7
myAge: 7

Setting myAge = 9...
myAge: 9
*pAge: 9

Image

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 14; this is verified by the printout on line 16.

On line 17, pAge is assigned the address of myAge. On line 18, pAge is dereferenced—using the indirection operator (*)—and printed, showing that the value at the address that pAge stores is the 5 stored in myAge.

On line 21, the value 7 is assigned to the variable at the address stored in pAge. This sets myAge to 7, and the printouts on lines 23 and 24 confirm this. Again, you should notice that the indirect access to the variable was obtained by using an asterisk—the indirection operator in this context.

On line 27, the value 9 is assigned to the variable myAge. This value is obtained directly on line 29 and indirectly (by dereferencing pAge) on line 30.

Examining the Address

Pointers enable you to manipulate addresses without ever knowing their real value. After today, 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 be certain? Listing 8.3 illustrates this idea.

Listing 8.3. Finding Out What Is Stored in Pointers


1:  // Listing 8.3
2:  // What is stored in a pointer.
3:  #include <iostream>
4:
5:  int main()
6:  {
7:     using namespace std;
8:
9:     unsigned short int myAge = 5, yourAge = 10;
10:
11:      // a pointer
12:     unsigned short int * pAge = &myAge;
13:
14:     cout << "myAge: " << myAge
15:        <<  " yourAge: " << yourAge << endl;
16:
17:     cout << "&myAge: " << &myAge
18:        << " &yourAge: " << &yourAge << endl;
19:
20:     cout << "pAge: " << pAge << endl;
21:     cout << "*pAge: " << *pAge << endl;
22:
23:
24:     cout << " Reassigning: pAge = &yourAge..." << endl << endl;
25:     pAge = &yourAge;       // reassign the pointer
26:
27:     cout << "myAge: " << myAge <<  
28:        " yourAge: " << yourAge << endl;
29:
30:     cout << "&myAge: " << &myAge
31:        << " &yourAge: " << &yourAge << endl;
32:
33:     cout << "pAge: " << pAge << endl;
34:     cout << "*pAge: " << *pAge << endl;
35:     
36:     cout << " &pAge: " << &pAge << endl;
37:
38:     return 0;
39:  }

Image

Image

Image

(Your output might look different.)

Image

On line 9, myAge and yourAge are declared to be variables of type unsigned short integer. On line 12, pAge is declared to be a pointer to an unsigned short integer, and it is initialized with the address of the variable myAge.

Lines 14–18 print the values and the addresses of myAge and yourAge. Line 20 prints the contents of pAge, which is the address of myAge. You should notice that the output confirms that the value of pAge matches the value of myAge’s address. Line 21 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 20 shows that pAge stores the address of myAge, and line 21 shows how to get the value stored in myAge by dereferencing the pointer pAge. Be certain that you understand this fully before you go on. Study the code and look at the output.

On line 25, 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 36 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.)

Image

Using Pointers

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 = *pPointer

Why Would You Use Pointers?

So 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:

• Managing data on the free store

• Accessing class member data and functions

• Passing variables by reference to functions

The remainder of today’s lesson focuses on managing data on the free store and accessing class member data and functions. Tomorrow, you will learn about passing variables using pointers, which is called passing by reference.

The Stack and the Free Store (Heap)

In the section “How Functions Work—A Peek Under the Hood” on Day 5, “Organizing into Functions,” five areas of memory are mentioned:

• Global namespace

• The free store

• Registers

• Code space

• The stack

Local variables are on the stack, along with function parameters. Code is in code space, of course, and global variables are in the global namespace. 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 of the remaining memory is given to the free store, which is often referred to as the heap.

Local variables don’t persist; when a function returns, its local variables are destroyed. This is good, because it means the programmer doesn’t have to do anything to manage this memory space, but is bad because it makes it hard for functions to create objects for use by other objects or functions without generating the extra overhead of copying objects from stack to return value to destination object in the caller. Global variables solve that problem at the cost of providing unrestricted access to those variables throughout the program, which leads to the creation of code that is difficult to understand and maintain. Putting data in the free store can solve both of these problems if that data is managed properly.

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. This is where destructors are absolutely critical, because they provide a place where any heap memory allocated in a class can be reclaimed.

The advantage to the free store is that the memory you reserve remains available until you explicitly state you are done with it by freeing it. If you reserve memory on the free store while in a function, the memory is still available when the function returns.

The disadvantage of the free store is also that the memory you reserve remains available until you explicitly state you are done with it by freeing it. If you neglect to free that memory, it can build up over time and cause the system to crash.

The advantage of accessing memory in this way, rather than using global variables, is that only functions with access to the pointer (which has the appropriate address) have access to the data. This requires the object containing the pointer to the data, or the pointer itself, to be explicitly passed to any function making changes, thus reducing the chances that a function can change the data without that change being traceable.

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.

Allocating Space with the new Keyword

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, assuming your system uses a two-byte unsigned short int and a four-byte long.

The return value from new is a memory address. Because you now know that memory addresses are stored in pointers, it should be no surprise to you that the return value from new should 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, do this all on one line by initializing the pointer at the same time you declare it:


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.”

Note

If new cannot create memory on the free store (memory is, after all, a limited resource), it throws an exception (see Day 20, “Handling Errors and Exceptions”).

Putting Memory Back: The delete Keyword

When you are finished with an area of memory, you must free it back to the system. You do this by calling delete on the pointer. delete returns the memory to the free store.

It is critical to remember that memory allocated with new is not freed automatically. If a pointer variable is pointing to memory on the free store and the pointer goes out of scope, the memory is not automatically returned to the free store. Rather, it is considered allocated and because the pointer is no longer available, you can no longer access the memory. This happens, for instance, if a pointer is a local variable. When the function in which that pointer is declared returns, that pointer goes out of scope and is lost. The memory allocated with new is not freed—instead, it becomes unavailable.

This situation is 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 prevent memory leaks, you should restore any memory you allocate back to the free store. You do this by using the keyword delete. For example:


delete pPointer;

When you delete the pointer, you are really 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.

Most commonly, you will allocate items from the heap in a constructor, and deallocate them in the destructor. In other cases, you will initialize pointers in the constructor, allocate memory for those pointers as the object is used, and, in the destructor, test the pointers for null and deallocate them if they are not null.

Caution

When you call delete on a pointer, the memory it points to is freed. Calling delete on that pointer again crashes 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; // allocate memory
delete pDog;   //frees the memory
     pDog = 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:  #include <iostream>
4:  int main()
5:  {
6:     using namespace std;
7:     int localVariable = 5;
8:     int * pLocal= &localVariable;
9:     int * pHeap = new int;
10:     *pHeap = 7;
11:     cout << "localVariable: " << localVariable << endl;
12:     cout << "*pLocal: " << *pLocal << endl;
13:     cout << "*pHeap: " << *pHeap << endl;
14:     delete pHeap;
15:     pHeap = new int;
16:     *pHeap = 9;
17:     cout << "*pHeap: " << *pHeap << endl;
18:     delete pHeap;
19:     return 0;
20:  }

Image


localVariable: 5
*pLocal: 5
*pHeap: 7
*pHeap: 9

Image

Line 7 declares and initializes a local variable ironically called localVariable. Line 8 declares a pointer called pLocal and initializes it with the address of the local variable. On line 9, a second pointer called pHeap is declared; however, it is initialized with the result obtained from calling new int. This allocates space on the free store for an int, which can be accessed using the pHeap pointer. This allocated memory is assigned the value 7 on line 10.

Lines 11–13 print a few values. Line 11 prints the value of the local variable (localVariable), line 12 prints the value pointed to by the pLocal pointer, and line 13 prints the value pointed to by the pHeap pointer. You should notice that, as expected, the values printed on lines 11 and 12 match. In addition, line 13 confirms that the value assigned on line 10 is, in fact, accessible.

On line 14, the memory allocated on 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 be used to point to other memory. It is reassigned on lines 15 and 16, and line 17 prints the result. Line 18 restores that memory to the free store.

Although line 18 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, having already taken care of this step is beneficial.

Another Look at Memory Leaks

Memory leaks are one of the most serious issues and complaints about pointers. You have seen one way that memory leaks can occur. 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. No way exists 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, on 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.

Creating Objects on the Free Store

Just as you can create a pointer to an integer, you can create a pointer to any data type, including classes. 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). Be aware, however, that you are not limited to using only the default constructor when creating an object with new—any constructor can be used.

Deleting Objects from 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 (generally deallocating heap allocated memory), 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 - Creating objects on the free store
2:  // using new and delete
3:
4:  #include <iostream>
5:
6:  using namespace std;
7:
8:  class SimpleCat
9:  {
10:  public:
11:     SimpleCat();
12:     ~SimpleCat();
13:  private:
14:     int itsAge;
15:  };
16:
17:  SimpleCat::SimpleCat()
18:  {
19:     cout << "Constructor called. " << endl;
20:     itsAge = 1;
21:  }
22:
23:  SimpleCat::~SimpleCat()
24:  {
25:     cout << "Destructor called. " << endl;
26:  }
27:  
28:  int main()
29:  {
30:     cout << "SimpleCat Frisky... " << endl;
31:     SimpleCat Frisky;
32:     cout << "SimpleCat *pRags = new SimpleCat..." << endl;
33:     SimpleCat * pRags = new SimpleCat;
34:     cout << "delete pRags... " << endl;
35:     delete pRags;
36:     cout << "Exiting, watch Frisky go... " << endl;
37:     return 0;
38:  }

Image


SimpleCat Frisky...
Constructor called.
SimpleCat *pRags = new SimpleCat..
Constructor called.
delete pRags...
Destructor called.
Exiting, watch Frisky go...
Destructor called.

Image

Lines 8–15 declare the stripped-down class SimpleCat. Line 11 declares SimpleCat’s constructor, and lines 17–21 contain its definition. Line 12 declares SimpleCat’s destructor, and lines 23–26 contain its definition. As you can see, both the constructor and destructor simply print a simple message to let you know they have been called.

On line 31, Frisky is created as a regular local variable, thus it is created on the stack. This creation causes the constructor to be called. On line 33, the SimpleCat pointed to by pRags is also created; however, because a pointer is being used, it is created on the heap. Once again, the constructor is called.

On line 35, delete is called on the pointer, pRags. This causes the destructor to be called and the memory that had been allocated to hold this SimpleCat object to be returned. When the function ends on line 38, Frisky goes out of scope, and its destructor is called.

Accessing Data Members

You learned on Day 6, “Understanding Object-Oriented Programming,” that you accessed data members and functions by using the dot (.) operator. As you should be aware, this works for Cat objects created locally.

Accessing the members of an object when using a pointer is a little more complex. 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. It is worth repeating this. You must first dereference the pointer. You then use the dereferenced value—the value being pointed to—along with the dot operator to access the members of the object. Therefore, to access the GetAge member function of an object pointed to by pRags, you write


(*pRags).GetAge();

As you can see, parentheses are used to ensure that pRags is dereferenced first—before GetAge() is accessed. Remember, parentheses have a higher precedence than other operators.

Because this is cumbersome, C++ provides a shorthand operator for indirect access: the class member access 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.

Note

Because the class member access operator (->) can also be used for indirect access to members of an object (through a pointer), it can also be referred to as an indirection operator. Some people also refer to it as the points-to operator because that is what it does.

Listing 8.6. Accessing Member Data of Objects on the Free Store


1:  // Listing 8.6 - Accessing data members of objects on the heap
2:  // using the -> operator
3:
4:  #include <iostream>
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:     using namespace std;
20:     SimpleCat * Frisky = new SimpleCat;
21:     cout << "Frisky is " << Frisky->GetAge() << " years old " << endl;
22:     Frisky->SetAge(5);
23:     cout << "Frisky is " << Frisky->GetAge() << " years old " << endl;
24:     delete Frisky;
25:     return 0;
26:  }

Image


Frisky is 2 years old
Frisky is 5 years old

Image

On line 20, a SimpleCat object that is pointed to by the pointer Frisky is instantiated (created) on the free store. The default constructor of the object sets its age to 2, and the GetAge() method is called on line 21. Because Frisky is a pointer, the indirection operator (->) is used to access the member data and functions. On line 22, the SetAge() method is called, and GetAge() is accessed again on line 23.

Creating Member Data on the Free Store

In addition to creating objects on the free store, you can also create data members within an object on the free store. One or more of the data members of a class can be a pointer to an object on the free store. Using what you have already learned, you can allocate memory on the free store for these pointers to use. The memory can be allocated in the class constructor or in one of the class’ methods. When you are done using the member, you can—and should—delete it in one of the methods or in the destructor, as Listing 8.7 illustrates.

Pointers as Member Data

Listing 8.7. Pointers as Member Data


1: // Listing 8.7 - Pointers as data members
2: // accessed with -> operator
3:
4: #include <iostream>
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:     using namespace std;
37:     SimpleCat *Frisky = new SimpleCat;
38:     cout << "Frisky is " << Frisky->GetAge()
39:                   << " years old " << endl;
40:     Frisky->SetAge(5);
41:     cout << "Frisky is " << Frisky->GetAge()
42:                   << " years old " << endl;
43:     delete Frisky;
44:     return 0;
45:  }

Image


Frisky is 2 years old
Frisky is 5 years old

Image

The class SimpleCat is declared to have two member variables—both of which are pointers to integers—on lines 18 and 19. The constructor (lines 22–26) initializes the pointers to memory on the free store and to the default values.

Notice on lines 24 and 25 that a pseudoconstructor is called on the new integer, passing in the value for the integer. This creates an integer on the heap and initializes its value (on line 24 to the value 2 and on line 25 to the value 5).

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 because 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 pointers 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 on line 41, 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.

Understanding What You Are Accomplishing

The use of pointers as was done in Listing 8.7 would be pretty silly in a real program unless a good reason existed for the Cat object to hold its members by reference. In this case, there is no good reason to use pointers to access itsAge and itsWeight, but in other cases, this can make a lot of sense.

This brings up the obvious question: What are you trying to accomplish by using pointers as references to variables instead of just using variables? Understand, too, that you must start with design. If what you’ve designed is an object that refers to another object, but the second object might come into existence before the first object and continue after the first object is gone, then the first object must contain the second by reference.

For example, the first object might be a window and the second object might be a document. The window needs access to the document, but it doesn’t control the lifetime of the document. Thus, the window needs to hold the document by reference.

This is implemented in C++ by using pointers or references. References are covered on Day 9, “Exploiting References.”

The this Pointer

Every class member function has a hidden parameter: the this pointer. this points to “this” individual object. Therefore, in each call to GetAge() or SetAge(), each function gets this for its object as a hidden parameter.

It is possible to use the pointer to this 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>
5:
6:  class Rectangle
7:  {
8:    public:
9:        Rectangle();
10:       ~Rectangle();
11:       void SetLength(int length)
12:          { this->itsLength = length; }
13:       int GetLength() const
14:          { return this->itsLength; }
15:
16:       void SetWidth(int width)
17:          { itsWidth = width; }
18:       int GetWidth() const
19:          { return itsWidth; }
20:
21:    private:
22:       int itsLength;
23:       int itsWidth;
24:  };
25:
26:  Rectangle::Rectangle()
27:  {
28:        itsWidth = 5;
29:        itsLength = 10;
30:  }
31:  Rectangle::~Rectangle()
32:  {}
33:
34:  int main()
35:  {
36:     using namespace std;
37:     Rectangle theRect;
38:     cout << "theRect is " << theRect.GetLength()
39:         << " feet long." << endl;
40:     cout << "theRect is " << theRect.GetWidth()
41:         << " feet wide." << endl;
42:     theRect.SetLength(20);
43:     theRect.SetWidth(10);
44:     cout << "theRect is " << theRect.GetLength()
45:         << " feet long." << endl;
46:     cout << "theRect is " << theRect.GetWidth()
47:         << " feet wide. " << endl;
48:     return 0;
49:  }

Image


theRect is 10 feet long.
theRect is 5 feet wide.
theRect is 20 feet long.
theRect is 10 feet wide.

Image

The SetLength() accessor function on lines 11–12 and the GetLength() accessor function on lines 13–14, both explicitly use the this pointer to access the member variables of the Rectangle object. The SetWidth() and GetWidth() accessors on lines 16–19 do not. No difference exists in their behavior, although the syntax is easier to understand.

If that were all there was to this, there would be little point in bothering you with it. this, 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 this on Day 10, “Working with Advanced Functions,” when operator overloading is discussed. For now, your goal is to know about this and to understand what it is: a pointer to the object that holds the function.

You don’t have to worry about creating or deleting the this pointer. The compiler takes care of that.

Stray, Wild, or Dangling Pointers

Yet again, issues with pointers are being brought up. This is because errors you create in your programs with pointers can be among the most difficult to find and among the most problematic. One source of bugs that are especially nasty and difficult to find in C++ is stray pointers. A stray pointer (also called a wild or dangling pointer) is created when you call delete on a pointer—thereby freeing the memory that it points to—and then you don’t set it to null. If you then try to use that pointer again without reassigning it, the result is unpredictable and, if you are lucky, your program will crash.

It is as though the Acme Mail Order company moved away, but you still pressed the programmed button on your phone. It is possible that nothing terrible happens—a telephone rings in a deserted warehouse. On the other hand, 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 without reallocating new memory for it 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.

Caution

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

Image

Image


*pInt:   10
*pLong:  90000
*pInt:   20
*pLong:  65556

(Do not try to re-create this output; yours will differ if you are lucky; or your computer will crash if you are not.)

Image

This is a listing you should avoid running because it could lock up your machine. On line 8, pInt is declared to be a pointer to USHORT, and is pointed to newly allocated memory. On line 9, the value 10 is put into that memory allocated for pInt. The value pointed to is then printed on line 10. After the value is printed, delete is called on the pointer. After line 11 executes, pInt is a stray, or dangling, pointer.

Line 13 declares a new pointer, pLong, which is pointed at the memory allocated by new. On line 14, the value 90000 is assigned to pLong, and on line 15, this value prints.

It is on line 17 that the troubles begin. On line 17, the value 20 is assigned 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 on line 11. Assigning a value to that memory is certain disaster.

On line 19, the value at pInt is printed. Sure enough, it is 20. Line 20 prints 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 on line 17?

As you might guess, these are related questions. When a value was placed at pInt on line 17, the compiler happily placed the value 20 at the memory location that pInt previously pointed to. However, because that memory was freed on line 11, the compiler was free to reassign it. When pLong was created on line 13, it was given pInt’s old memory location. (On some computers, this might 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 the memory address of pLong in Listing 8.9:

1. pInt was pointed at a particular memory location, and the value 10 was assigned.

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.

FAQ

What is the difference between a null pointer and a stray pointer?

Answer: When you delete a pointer, you tell the compiler to free the memory, but the pointer itself continues to exist. It is now a stray pointer.

When you then write myPtr = 0; you change it from being a stray pointer to being a null pointer.

Normally, if you delete a pointer and then delete it again, your program is undefined. That is, anything might happen—if you are lucky, the program will crash. If you delete a null pointer, nothing happens; it is safe.

Using a stray or a null pointer (for example, writing myPtr = 5;) is illegal, and it might crash. If the pointer is null, it will crash, another benefit of null over stray. Predictable crashes are preferred because they are easier to debug.

Using const Pointers

You can use the keyword const for pointers before the type, after the type, or in both places. For example, all the following are legal declarations:


const int * pOne;
int * const pTwo;
const int * const pThree;

Each of these, however, does something different:

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. The following helps to illustrate this:


const int * p1;  // the int pointed to is constant
int * const p2;  // p2 is constant, it can’t point to anything else

const Pointers and const Member Functions

On Day 6, 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 - Using pointers with const methods
2:
3:  #include <iostream>
4:  using namespace std;
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:       void SetWidth(int width) { itsWidth = width; }
14:       int GetWidth() const { return itsWidth; }
15:
16:    private:
17:       int itsLength;
18:       int itsWidth;
19:  };
20:
21:  Rectangle::Rectangle()
22:  {
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()
37:         << " feet"  << endl;
38:     cout << "pConstRect width: " << pConstRect->GetWidth()
39:         << " feet" << endl;
40:     cout << "pConstPtr width: " << pConstPtr->GetWidth()
41:         << " feet" << endl;
42:
43:     pRect->SetWidth(10);
44:     // pConstRect->SetWidth(10);
45:     pConstPtr->SetWidth(10);
46:
47:     cout << "pRect width: " << pRect->GetWidth()
48:         << " feet ";
49:     cout << "pConstRect width: " << pConstRect->GetWidth()
50:         << " feet ";
51:     cout << "pConstPtr width: " << pConstPtr->GetWidth()
52:         << " feet ";
53:     return 0;
54:  }

Image


pRect width: 5 feet
pConstRect width: 5 feet
pConstPtr width: 5 feet
pRect width: 10 feet
pConstRect width: 5 feet
pConstPtr width: 10 feet

Image

Lines 6–19 declare the Rectangle class. Line 14 declares the GetWidth() member method const.

Line 32 declares a pointer to Rectangle called pRect. On line 33, a pointer to a constant Rectangle object is declared and named pConstRect. On line 34, pConstPtr is declared as a constant pointer to a Rectangle. Lines 36–41 print the values of these three variables.

On line 43, pRect is used to set the width of the rectangle to 10. On line 44, pConstRect would be used to set the width, but it was declared to point to a constant Rectangle. Therefore, it cannot legally call a non-const member function. Because it is not a valid statement, it is commented out.

On line 45, pConstPtr calls SetWidth(). 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, so methods such as GetWidth() and SetWidth() can be used.

Using a const this Pointers

When you declare an object to be const, you are in effect declaring that the object’s this pointer is a pointer to a const object. A const this pointer can be used only with const member functions.

Image

Constant objects and constant pointers will be discussed again tomorrow, when references to constant objects are discussed.

Summary

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.

Q&A

Q   Why are pointers so important?

A   Pointers are important for a number of reasons. These include being able to use pointers to hold the address of objects and to use them to pass arguments by reference. On Day 14, “Polymorphism,” you’ll see how pointers are used in class polymorphism. In addition, many operating systems and class libraries create objects on your behalf and return pointers to them.

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. In addition, the capability 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.

Workshop

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 be certain you understand the answers before continuing to tomorrow’s lesson.

Quiz

1. What operator is used to determine the address of a variable?

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?

Exercises

1. What do these declarations do?

a. int * pOne;

b. int vTwo;

c. int * pThree = &vTwo;

2. If you have an unsigned short variable named yourAge, how would you declare a pointer to manipulate yourAge?

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>
using namespace std;
int main()
{
       int *pInt;
       *pInt = 9;
       cout << "The value at pInt: " << *pInt;
       return 0;
}

6. BUG BUSTERS: What is wrong with this code?


#include <iostream>
using namespace std;
int main()
{
     int SomeVariable = 5;
     cout << "SomeVariable: " << SomeVariable << endl;
     int *pVar = & SomeVariable;
     pVar = 9;
     cout << "SomeVariable: " << *pVar << endl;
     return 0;
}

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.27.171