Day 8
Pointers
One of the most powerful tools available to a C++ programmer is the
ability to manipulate computer memory directly by using pointers. 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 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 book progresses.
What Is a Pointer?
New Term: A pointer is a variable that holds a memory
address.
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 Day 5, "Functions.") Figure 8.1 shows a schematic representation
of the storage of an unsigned long integer
variable 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.
//
Listing 8.1 Demonstrates address of operator
//
and addresses of local variables
3:
4: #include <iostream.h> 5:
int
main()
{
unsigned
short shortVar=5;
long
sVar = -65535;
11:
cout
<< "shortVar:\t" << shortVar;
cout
<< " Address of shortVar:\t";
cout
<< &shortVar _<< "\n";
15:
cout
<< "longVar:\t" <<
longVar;
cout << " Address of longVar:\t" ;
cout
<< &longVar _<< "\n";
19:
cout
<< "sVar:\t"<< sVar;
21:
|
cout
|
<<
|
" Address of sVar:\t" ;
|
|
22:
|
cout
|
<<
|
&sVar
|
_<<
"\n";
|
23:
|
|
|
|
|
return
0;
}
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.
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.
Storing the Address in a Pointer
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
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 pAge
|
The 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
|
howOld
|
pAge
is a pointer that now contains the address of the
howOld variable. Using
pAge, you can actually determine the value of howOld, which
in this case is 50. Accessing howOld by using the pointer pAge is
called indirection because you are indirectly accessing howOld by means of pAge. Later
today you will see
how to
use indirection to access a variable's value.
New Term: 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.
Pointers can have any name that is legal for other variables. This book
follows the convention of naming all pointers with an initial p, as in pAge or pNumber.
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 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.
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.
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.
Manipulating Data by Using Pointers
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:
typedef
unsigned short int USHORT;
int
main()
{
8:
|
USHORT
|
myAge;
|
//
|
a
|
variable
|
9:
|
USHORT
|
*
pAge = 0;
|
//
|
a
|
pointer
|
myAge
= 5;
cout
<< "myAge: " << myAge << "\n";
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:
|
|
|
cout
<< "*pAge: " << *pAge << "\n";
cout
<< "myAge: " << myAge << "\n\n";
25: cout << "myAge = 9\n"; 26:
27: myAge = 9; 28:
cout
<< "myAge: " << myAge << "\n";
return
0;
}
Output: myAge: 5 *pAge: 5
*pAge = 7 *pAge: 7 myAge: 7
myAge = 9 myAge: 9 *pAge: 9
Analysis: 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.
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 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:
typedef
unsigned short int USHORT;
int
main()
{
unsigned
short int myAge = 5, yourAge = 10;
unsigned
short int * pAge = &myAge; // a
pointer
cout
<< "myAge:\t" << myAge << "\tyourAge:\t" << yourAge
<<
"\n";
cout <<
"&myAge:\t" << &myAge <<
"\t&yourAge:\t" << &yourAge <<"\n";
14: cout
<< "pAge:\t" << pAge << "\n";
17: pAge = &yourAge; // reassign the pointer 18:
cout
<< "myAge:\t" << myAge << "\tyourAge:\t" << yourAge
<<
"\n";
cout <<
"&myAge:\t" << &myAge <<
"\t&yourAge:\t" << &yourAge <<"\n";
cout
<< "pAge:\t" << pAge << "\n";
cout
<< "*pAge:\t" << *pAge << "\n";
cout
<< "&pAge:\t" << &pAge << "\n";
return
0;
}
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 = *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.
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.
The Stack and the Free Store
In the "Extra Credit" section following the discussion of
functions in Day 5, five areas of memory are mentioned:
Global name space
The free
store
● Registers
The stack
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.
new
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
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.
delete
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 memory
Listing 8.4. Allocating, using, and deleting pointers.
//
Listing 8.4
//
Allocating and deleting a pointer
#include
<iostream.h>
int
main()
{
int
localVariable = 5;
int
* pLocal= &localVariable;
int
* pHeap = new int;
if
(pHeap == NULL)
{
12:
|
cout << "Error! No memory for pHeap!!";
|
13:
|
return
0;
|
}
*pHeap
= 7;
cout
<< "localVariable: " << localVariable <<
"\n";
cout
<< "*pLocal: " << *pLocal << "\n";
cout
<< "*pHeap: " << *pHeap << "\n";
delete
pHeap;
pHeap
= new int;
if
(pHeap == NULL)
{
23:
|
cout << "Error! No memory for pHeap!!";
|
24:
|
return
0;
|
}
*pHeap
= 9;
cout
<< "*pHeap: " << *pHeap << "\n";
delete
pHeap;
return
0;
}
Output: localVariable: 5 *pLocal: 5
*pHeap: 7 *pHeap: 9
Analysis: 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.
Memory Leaks
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:
unsigned
short int * pPointer = new unsigned short int;
*pPointer
= 72;
pPointer
= new unsigned short int;
*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:
unsigned
short int * pPointer = new unsigned short int;
*pPointer
= 72;
delete
pPointer;
pPointer
= new unsigned short int;
*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.
Creating Objects on the Free Store
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).
Deleting Objects
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.
//
Listing 8.5
//
Creating objects on the free store
4: #include <iostream.h> 5:
class
SimpleCat
{
public:
9:
|
SimpleCat();
|
10:
|
~SimpleCat();
|
11
|
private:
|
12
|
int itsAge;
|
13
|
};
|
14
|
|
SimpleCat::SimpleCat()
{
17
|
cout << "Constructor called.\n";
|
18
|
itsAge
= 1;
|
19
|
}
|
20
|
|
SimpleCat::~SimpleCat()
{
23
|
cout << "Destructor called.\n";
|
24
|
}
|
25
|
|
int
main()
{
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;
|
return
0;
}
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.
Accessing Data Members
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.
//
Listing 8.6
//
Accessing data members of objects on the heap
4: #include <iostream.h> 5:
class
SimpleCat
{
public:
9:
|
SimpleCat() {itsAge
|
=
2; }
|
|
10:
|
~SimpleCat()
|
{}
|
|
11:
|
int
GetAge()
|
const
|
{ return itsAge; }
|
private:
14:
|
int itsAge;
|
15:
|
};
|
16:
|
|
int
main()
{
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;
|
|
return
0;
}
Output: Frisky is 2
years old
Frisky is 5 years old
Analysis: 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.
Member Data 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. 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.
//
Listing 8.7
//
Pointers as data members
4: #include <iostream.h> 5:
class
SimpleCat
{
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:
|
|
18:
|
int
* itsAge;
|
19:
|
int * itsWeight;
|
20:
|
};
|
21:
|
|
SimpleCat::SimpleCat()
{
24: itsAge = new int(2); 25: itsWeight = new int(5); 26: } 27:
SimpleCat::~SimpleCat()
{
30:
|
delete
itsAge;
|
31:
|
delete itsWeight;
|
32:
|
}
|
33:
|
|
int
main()
{
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;
|
|
return
0;
}
Output: Frisky is 2
years old
Frisky is 5 years old
Analysis: 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.
The this Pointer
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
It is possible to use the this pointer
explicitly, as Listing 8.8 illustrates.
Listing 8.8. Using the this pointer.
//
Listing 8.8
//
Using the this pointer
4: #include <iostream.h> 5:
class
Rectangle
{
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:
|
|
private:
18:
|
int itsLength;
|
19:
|
int
itsWidth;
|
20:
|
};
|
21:
|
|
Rectangle::Rectangle()
{
24:
|
itsWidth
= 5;
|
25:
|
itsLength = 10;
|
}
Rectangle::~Rectangle()
{}
29:
int
main()
{
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;
|
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 Day 10, "Advanced Functions," when operator overloading is
discussed. For now, your goal is to know about the this pointer and to understand what it is: a pointer to the object itself.
You don't have to worry about
creating or deleting the this pointer. The compiler takes care
of that.
Stray or Dangling Pointers
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
typedef
unsigned short int USHORT;
#include
<iostream.h>
5:
int
main()
{
USHORT
* pInt = new USHORT;
*pInt
= 10;
cout
<< "*pInt: " << *pInt << endl;
delete
pInt;
pInt
= 0;
long
* pLong = new long;
*pLong
= 90000;
cout
<< "*pLong: " << *pLong << endl;
17:
|
*pInt
= 20;
|
// uh oh, this was deleted!
|
18:
|
|
|
cout
<< "*pInt: " << *pInt
<< endl;
cout
<< "*pLong: " << *pLong
<< endl;
delete
pLong;
return
0;
}
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:
pInt was pointed at a particular
memory location, and the value 10 was assigned.
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.
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.
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.
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.
DO use new to create objects on the free store.
DO use delete to destroy objects on the free store and to return their memory. DON'T
forget to balance all new statements with a delete statement. DON'T forget to
assign null (0) to all pointers that you call delete on. DO check the value returned by new.
const Pointers
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
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, "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.
//
Listing 8.10
//
Using pointers with const methods
4: #include <iostream.h> 5:
class
Rectangle
{
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:
|
|
private:
18:
|
int itsLength;
|
19:
|
int
itsWidth;
|
20:
|
};
|
21:
|
|
Rectangle::Rectangle():
itsWidth(5),
itsLength(10)
{}
26:
Rectangle::~Rectangle()
{}
29:
int
main()
{
32: Rectangle*
pRect = new
Rectangle;
33: const
Rectangle * pConstRect = new Rectangle;
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";
|
|
return
0;
}
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.
const this Pointers
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.
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
Why are
pointers so important?
Today you saw how pointers are
used to hold the address of objects on the free store and how they are used to
pass arguments by reference. In addition, on Day 13, "Polymorphism,"
you'll see how pointers are used in class polymorphism.
Why
should I bother to declare anything on the free store?
Objects on the free store persist
after the return of a function. Additionally, the ability to store objects on
the free store enables you to decide at runtime how many objects you need,
instead of having to declare this in advance. This is explored in greater depth
tomorrow.
Why
should I declare an object const if it limits what I can do with it?
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 make sure you
understand the answers before continuing to the next chapter.
Quiz
What operator is used to find the value stored at
an address held in a pointer?
What is a pointer?
What is the difference between the address stored
in a pointer and the value at that address?
What is the difference between the indirection
operator and the address of operator?
What is the difference between const int *
ptrOne and int * const ptrTwo?
Exercises
What do these declarations do?
int * pOne;
int vTwo;
int * pThree = &vTwo;
If you have an unsigned
short variable named yourAge, how
would you declare a pointer to manipulate yourAge?
Assign the value 50 to the variable yourAge by using
the pointer that you declared in Exercise 2.
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.
BUG BUSTERS: What is wrong with this code?
#include <iostream.h> int main()
{ int *pInt; *pInt = 9;
cout << "The value at pInt: " << *pInt;
return 0;
}
6. BUG BUSTERS: What is wrong with this code?
int main()
{
int
SomeVariable = 5;
cout << "SomeVariable: " << SomeVariable
<< "\n"; int *pVar = & SomeVariable;
pVar
= 9;
cout << "SomeVariable: " << *pVar
<< "\n"; return 0;
}
0 comments:
Post a Comment