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:
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.
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:
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:
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:
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.
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:
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.
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:
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++
|