Search


Software Design Using C++



Pointers


We have used pointers informally throughout these Web pages and have pictured them as arrows. A pointer is probably implemented as the main memory address (or some variation on this) of the item to which it points. Pointers have been used to implement reference parameters on the run-time stack, arrays names were seen as pointers, and the this pointer was used with objects, just to name three uses. In addition we will later use pointers to construct dynamic data structures such as linked lists, stacks, queues, and binary trees.

A pointer variable is normally a pointer to a particular type of item. For example, we could have p be a pointer to an int. This would be set up as follows:


int * p;

In the above, the * means the item that p points to. Thus it is saying that the item that p points to is an int. Of course, it is often more convenient to use a typedef to create a type name for any "pointer to an int".

A Simple Example


Let's look at a simple example, pointers.cpp, to see how we might use pointers. Notice that a typedef has been set up to give the type name IntPtrType. NumPtr is then declared to have this type. AgeOfMary and AgeOfBill are set up as int variables. The code then does the following:


AgeOfMary = 19;
AgeOfBill = 21;
                                                                               
NumPtr = &AgeOfMary;  // get address of the AgeOfMary variable
cout << "After setting NumPtr to point to AgeOfMary, NumPtr is " 
   << NumPtr << endl;
cout << "NumPtr points to " << *NumPtr << endl;
cout << "AgeOfMary is " << AgeOfMary << endl
   << "AgeOfBill is " << AgeOfBill << endl << endl;

Note that in this context, the ampersand does not indicate a reference parameter. Rather, it indicates the "address of" the item that follows. Also, remember that the * indicates the item pointed to by the variable that follows. The output from the above section of code might be something like that shown below. Note that the address contained in NumPtr can vary from computer to computer and even from one run of the program to another.

After setting NumPtr to point to AgeOfMary, NumPtr is 0x0064FDF0
NumPtr points to 19
AgeOfMary is 19
AgeOfBill is 21

We might picture the above situation as follows.

[pointer drawing]

The next section of code moves NumPtr so that it points to the AgeOfBill variable. The code is shown below.


NumPtr = &AgeOfBill;
cout << "After moving NumPtr to point to AgeOfBill, NumPtr is "
   << NumPtr << endl;
cout << "NumPtr points to " << *NumPtr << endl;
cout << "AgeOfMary is " << AgeOfMary << endl
   << "AgeOfBill is " << AgeOfBill << endl << endl;

The output for this section of code looks like this. Again, the exact address contained in the pointer may be different when you run the program.

After moving NumPtr to point to AgeOfBill, NumPtr is 0x0064FDEC
NumPtr points to 21
AgeOfMary is 19
AgeOfBill is 21

The modified picture for the above situation is as follows:

[pointer drawing]

As shown below, the next section of code changes *NumPtr, the item that the pointer currently points to. Thus, in this case we are changing what is in the variable AgeOfBill. Note that it is important to distinguish a pointer from the item to which it points.


*NumPtr = 22;
cout << "After changing *NumPtr, NumPtr is " << NumPtr
   << endl;
cout << "NumPtr points to " << *NumPtr << endl;
cout << "AgeOfMary is " << AgeOfMary << endl
   << "AgeOfBill is " << AgeOfBill << endl << endl;

Here is the output for the current section of code. Note that we can access the 22 via the pointer NumPtr or by looking in the AgeOfBill variable.

After changing *NumPtr, NumPtr is 0x0064FDEC
NumPtr points to 22
AgeOfMary is 19
AgeOfBill is 22

The picture for the above is the same as before, except that you can see that a 22 has replaced the 21:

[pointer drawing]

Finally, we execute the last section of code, which is shown below. It changes our pointer variable so that it contains the special value NULL, which does not point at anything. Thus you cannot follow the pointer to try to examine the item pointed to, as is mentioned in the code itself.


NumPtr = NULL;
cout << "After changing NumPtr to NULL, NumPtr is " << NumPtr
   << endl;
cout << "We cannot print *NumPtr as NumPtr is NULL" << endl;
cout << "AgeOfMary is " << AgeOfMary << endl
   << "AgeOfBill is " << AgeOfBill << endl << endl;

The output from the above section is:

After changing NumPtr to NULL, NumPtr is 0x00000000
We cannot print *NumPtr as NumPtr is NULL
AgeOfMary is 19
AgeOfBill is 22

The following picture shows the above situation:

[pointer drawing]

Using Pointers to Traverse Arrays


A pointer variable can be used to traverse the items in an array, as is shown in arrayptr.cpp. It is said that this is usually faster than using array indexing.

This example begins by setting up a pointer variable and an array. The array is initialized to contain some integer values. This section of code is approximately as shown below.


IntPtrType DataPtr;
int A[MAX] = {100, 90, 80, 70, 60, 50, 40, 30, 20, 10};

Here is one way to write a loop to go through all of the items in the array, printing each item.


for (DataPtr = A; DataPtr < A + MAX; DataPtr++)
   cout << *DataPtr << endl;

DataPtr is initially given a copy of the value of A. This might seem strange unless you remember that an array name is seen as a pointer to the first data item in the array. Thus, the value of A is &A[0], the address of A[0].

Also note that A + MAX - 1 would be a pointer to the last item in the array. Pointer arithmetic is smart enough to take into account the size of the data items in the array. So, whether we have an array of integers or an array of rather large records, A + 1 always points to A[1], A + 2 points to A[2], etc.

We now see that the test in the above for loop makes sure that we do not run off of the end of the array. Note that one pointer is less than another provided that the first points to something that comes before (in memory) what the second points to.

The update step in the above loop simply adds 1 to the pointer, causing it to point to the next item in the array. Of course, the loop body prints out *DataPtr, the item pointed to by DataPtr.

The following is a picture of the array and associated pointers, with DataPtr shown at the point where it is pointing to A[3]. Note that A + MAX points off the end of the array.

[array with pointers]

Dynamic Memory Allocation


Pointers also come into play when we want to allocate a chunk of memory while our program is running. This will be used later with various dynamic data structures. As a first example, let's look at dynamic.cpp, which begins by showing how to allocate space for an integer. The section of code which does this is as follows:


IntPtr = new int;
if (IntPtr == NULL)
   {
   cerr << "Could not allocate sufficient space" << endl;
   exit(1);
   }

The new operation allocates space in the free store (sometimes called the heap), which is a special region in the main memory area dedicated to your program. The new operation assigns the amount of space needed by the type of variable mentioned after it, in this case an int. If the new succeeds, it returns a pointer to the allocated space. If it fails, it returns a value of NULL.

One then uses *IntPtr anytime access is needed to the space pointed to. For example, our program places a number in this space as follows:


*IntPtr = 55;

When one is finished with dynamically allocated space, one should return it to the free store by using the delete operation as shown below. This allows the space to later be reused for something else. Although dynamically allocated space should ideally be freed up when your program ends, this does not always happen. A program that doesn't return all of the space that it allocates is said to suffer from memory leaks. After running the program one has less available memory than before, at least until the computer is restarted. Always reclaim any dynamically allocated space.


delete IntPtr;

The next section shows how to dynamically allocate space for an array of integers. The actual allocation proceeds as follows:


IntPtr = new int[NumItems];
if (IntPtr == NULL)        
   {                       
   cerr << "Could not allocate sufficient space" << endl;
   exit(1);                
   }            

You can then use IntPtr as if it were an array, because for all practical purposes it is! (The main difference is that an ordinary array is kept in a different place in main memory than a dynamically allocated array.) Remember that an array name is seen as a pointer to the first data item, and that is precisely what IntPtr is. Thus you see the program accessing IntPtr[k] to get at the item at index k in the array.

One again uses the delete operation to free up space when finished with the allocated memory. However, with an array the [] brackets are given between the delete and the pointer variable as shown below:

                                                                    
delete [] IntPtr;

Next, our program shows how to use pointers as strings. In one method, we set up pointer variable Msg1 as a pointer to a character, initialized as shown below. Space is allocated for a character array to hold This is a message since that is how the compiler handles literal strings such as this. The assignment statement merely copies the pointer to the first character (the 'T') into the Msg1 variable. Note that the characters of the string do NOT get copied! One can then use something like cout << Msg1; to output the string.


typedef char * CharPtrType;
                                                                               
CharPtrType Msg1 = "This is a message";

The other way shown for setting up a pointer as a string dynamically allocates space for the string. Msg2 is a pointer variable just like Msg1. Note the use of strcpy to copy the characters of the string into the newly allocated space. Note well that strcpy does not check to see if the destination string has enough room for the data. This can open up one's software to a buffer overflow attack, a well-known type of security flaw. When writing professional software use a copy function that does proper bounds checking instead. Follow the above link for further information on this important topic.


Msg2 = new char[16];                                         
if (Msg2 == NULL)                                            
   {                                                         
   cerr << "Could not allocate sufficient space" << endl;
   exit(1);                                                  
   }                                                                        
                                                                               
strcpy(Msg2, "A new message");
cout << Msg2 << endl;
// Delete the space for the message array:
delete [] Msg2;               

Overall, what is the main difference between the 2 methods, that used with Msg1 and that used with Msg2? Both used a literal string (characters between double quotes). However, with Msg1 we just had the pointer point to the existing literal string. With Msg2 we created a copy of the entire string of data. The second method would seem to be more wasteful, but there are times when a copy of the original string is exactly what we want.

Dynamically Allocating Objects


The dyobject.cpp example shows how to dynamically allocate and delete an object. The class of Product objects is declared and defined in product.h and product.cpp.

The code to create, print, and remove such a Product object is shown below. Note that we again use a pointer to the dynamically created item. The new operation uses the constructor function to initialize the data in the object. In this example the constructor takes three parameters: the name of the product, the year it was manufactured, and the price.


ProductPtrType DynamicProdPtr;

DynamicProdPtr = new Product("Another product (dynamic)", 1997, 9.33);
if (DynamicProdPtr == NULL)
   {
   cerr << "Could not allocate sufficient space" << endl;
   exit(1);
   }

DynamicProdPtr->Print();
cout << "Printing the dynamic object in another way:" << endl;
(*DynamicProdPtr).Print();
// Delete space used by the dynamic object -- uses the destructor:
delete DynamicProdPtr;

Note the use above of the -> to access a class function of the object pointed to by DynamicProdPtr. This is really just a convenient shortcut for the clumsier notation (*DynamicProdPtr).Print(). It is clear from the latter that we find the object pointed to and then call its Print function. (The same arrow notation is also used to pick out a field of a structure when given a pointer to the structure.)

Finally, note that when the delete operation is used on the pointer to an object, the destructor class function is called. An automatic destructor is always provided which reclaims the space for the object itself, but sometimes you need to write your own. This is the case with our Product objects, because the name string itself is not stored within the object. Instead, each object contains a pointer to the name string, which is external to the object. This was done deliberately to show how to handle such a case. Using the default destructor would reclaim the space for the object itself, but would not free up the space used by this external string.

The answer to this problem is to write one's own destructor that explicitly frees up the space used by this string. A destructor function always uses as its function name the name of the class preceded by a ~ symbol. Note the entry in the class declaration for this ~Product function. The actual code for the function is pasted in below for convenience.


Product::~Product(void)
   {
   // Print a message just so that you can see when the destructor
   // gets called:
   cout << "Destructor called for product named " << NamePtr
      << endl;
   // Explicitly reclaim the space used by the string:
   delete [] NamePtr;
   } 

Normally a destructor would not print a message. This is done here just so that when you run the program you will be able to see when the destructor gets called. Try it! You will see that besides getting called for the dynamically allocated object, the destructor gets called when an ordinary "static" object goes out of scope (in this case because the program ends).

Related Items

Back to the main page for Software Design Using C++

Author: Br. David Carlson
Last updated: August 03, 2006