Introduction
As was noted in Lab Exercise 4, a variable data object can be thought of as having three components:
- The variable's name is the way we normally refer to it in
a program.
- The variable's address is the memory location associated
with its name. The variable's type indicates the kind of
value to be stored in its memory location, which, in turn,
determines its size (or the number of bytes needed for the variable).
- The variable's value is comprised of the contents of its
memory location.
If characters are stored in one byte of memory and integers are
stored in four bytes of memory, then when the compiler processes the
declarations
char ch;
int intVal;
it allocates (or sets aside) memory for the variables
ch and intVal. If the memory location set aside for
ch is 0x08, and the compiler allocates ch
and intVal in adjacent memory locations, then we might
picture memory as follows:
| Address |
... |
0x08 |
0x09 |
0x0A |
0x0B |
0x0C |
... |
| Value |
|
|
|
|
|
|
|
| Name |
|
ch |
intVal |
|
Note: in this example, memory is
allocated from higher order addresses to lower; in other systems, it
may be allocated from lower to higher order addresses
Such a picture is called a memory map because it represents a
mapping between a program's variable names and its memory addresses,
which typically are represented in hexadecimal (base-16)
notation. Note that the memory address associated with
intVal is 0x09, even though intVal actually
consists of locations 0x09 through 0x0C, as
indicated by the shaded part of the picture.
A variable's name can be thought of as a symbolic replacement for its
address because an access to a variable is really an access to its
memory location. To illustrate, assigning a value to a variable,
ch = 'A';
simply changes the value of the variable's memory location. If ASCII
code is in use, we can picture the result of such an assignment as
follows:
| Address |
... |
0x08 |
0x09 |
0x0A |
0x0B |
0x0C |
... |
| Value |
|
65 |
|
|
|
|
|
| Name |
|
ch |
intVal |
|
As you do this lab exercise, keep in mind the distinction between
these three parts of variables.
Note: In this lab exercise, you will be using the program
pointers.cpp. Get a copy of this program by right-clicking
and saving the following link: source_lab11_pointers.cpp
and then rename the file to pointers.cpp.
This program currently does very little besides declare some
int variables int1, int2, and int3 and
double variables dub1, dub2, and dub3. It
will be an "experimental laboratory" for the first part of this lab
exercise. You will be adding statements to it throughout the lab
exercise and will be handing in a listing along with this lab
handout.
A. Addresses
Lab Exercise 4 described how
addresses of variables can be found and displayed. We begin this lab
by reviewing how this was done using the address-of operator
(&). The expression
produces the address of the memory location in which a variable named
variable is stored.
Add statements to pointers.cpp to display the addresses
of int1, int2, int3, dub1, dub2, and dub3 and
record these addresses below. Also, draw a memory map of the
memory allocated to the six variables.
B. Declaring Pointer Variables
There are situations in which it is useful to define a variable whose
purpose is to store an address. Such variables, whose values are
memory addresses, are called pointer variables (or simply
pointers) because their values lead (or point) to other
addresses.
One of the uses of a pointer variable is to hold the address of
another variable. Because variables can be of different types and
different types are of different sizes, a pointer variable must be
declared as a pointer to a type. The general declaration notation
declares pointerName as a variable capable of holding the
address of a variable of type Type. Note the asterisk before
the variable name. It must be used before each pointer variable:
| Type * pointerName1, * pointerName2, * pointerName3, ...; |
- In the space below, declare two int pointer
variables named intPtr1 and intPtr2 and two
double pointer variables named dubPtr1 and
dubPtr2.
- Add these declarations to pointers.cpp as well as
statements to display the addresses these four pointer
variables. Record these addresses below:
Address of intPtr1: ______________________ Address of intPtr2: ______________________
Address of dubPtr1: ______________________ Address of dubPtr2: ______________________
- Notice that the same number of bytes is allocated to all of
these pointer variables. How many were they?
______________________
This is because the value of a pointer variable is a memory
address and this many bytes are needed to store an address.
Even though intPtr1 and intPtr2 are two
integer-pointer variables and dubPtr1 and
dubPtr2 are two double-pointer variables, the value
of each one will be a memory address only.
C. Assigning Values to Pointer Variables
The value of a pointer variable of type T is an address of a
memory location where a value of type T can be stored.
- Add statements to pointers.cpp to assign the
address of int2 (i.e., &int2) to
intPtr2 and the address of dub2 (i.e.,
&dub2) to dubPtr2 and statements to
display the values of intPtr2 and dubPtr2.
Record the output below.
Reminder: These values are
addresses so you may need to typecast them to type
(unsigned) or (void *), as in the Notes in
Lab Exercise 4.
- Is the value of intPtr2 the same as the address of
int2 that you recorded in part A? ______________________
- Is the value of dubPtr2 the same as the address of
dub2 that you recorded in part A? ______________________
- Let's try assigning one pointer to another. Add to
pointers.cpp the assignment statement
intPtr1 = intPtr2;
and a statement to display the value of intPtr1.
Compile, execute, and report the output produced:
- Now try assigning intPtr1 to dubPtr1 and
report what happens:
You should have gotten an error in (b); the reason for this is that
we're trying to assign an int-pointer (of type int
*) to a double-pointer variable (of type double
*). For this to be done correctly, we would need to convert the
int * value to a double * value. For example, with
a typecast as in the statement
| dubPtr1 |
= |
(double *) intPtr1; |
|
|
↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ |
|
Typecast the value to a double-pointer |
Make this change to the statement you added to
pointers.cpp. Recompile and execute again. Does this
typecast solve the problem? ____________________
We now see the following important rule:
For a pointer of the form
ptr1 = ptr2
to be valid, ptr1 and ptr2 must be declared
to be pointers to the same type. This is usually stated as:
ptr1 and ptr2 must be bound to the same type.
|
Also, we can now explain (somewhat) what this (void *)
stuff is all about:
An expression of the form
(void *) address
is a typecast of address to a void
pointer — a pointer of type void * —
which is a generic pointer that can represent any pointer
type. Casting addresses (and pointers) to type void
* may be necessary for them to display correctly as
addresses.
|
One last thing about assignment; in pointers.cpp:
- Declare an int pointer intPtr3 and assign
it the value 0.
- Declare a double pointer dubPtr3 and
assign it the value 0.
- Display the values of intPtr3 and dubPtr3.
Report what happens:
|
As a pointer value, 0 is called the null
address. It is type-independent and can be assigned to
any pointer variable.
|
D. Dereferencing Pointer Variables
One of the reasons pointer variables are useful is that they provide
an alternative means of accessing the memory location whose address is
their value. That is, we just saw that the value of the expression
intPtr2
is the value of intPtr2 (currently the address of int2).
Add a statement to pointers.cpp to display the value of
the expression
*intPtr2
What is displayed?
When applied to a pointer variable in an expression,
*pointerName
the * operator accesses the value at the memory
location whose address is the value of
pointerName, instead of accessing the value of
pointerName.
|
Thus, the expression
cout << *intPtr2
causes two actions to occur:
- The value of intPtr2 is accessed, which is an
address (currently of int2); and
- The memory location at that address (containing the value
22) is accessed.
In this case, the purpose of the access is to retrieve the value
so that it can be displayed but other accesses to that memory
location are also permitted.
- Now add the statement
*intPtr2 = 99;
to pointers.cpp and then display the value of
int1, int2, and int3. What change has
occurred?
- In your own words, explain how the assignment accomplished this.
This operation of accessing a remote memory location
indirectly via a pointer variable is called
dereferencing the pointer. Intuitively, dereferencing
a pointer causes an access to the memory location it points
at, instead of an access to its own memory location.
|
Note that although the same symbol (*) is used to
both declare and dereference pointer variables, the two
uses are completely distinct and should not be confused.
|
But ... one must be careful with using the dereferencing
operator! Add some statement to pointers.cpp that
dereferences intPtr3 (e.g., cout << *intPtr3
<< endl;) and report what happens when you compile and
execute the program.
You may have just experienced the dreaded seg fault, the
curse of all those who program with pointers!
|
Any attempt to dereference a null (or undefined or void) pointer
is an error and usually produces the infamous "segmentation
fault" or "bus error" run-time error (but it may simply
produce some garbage value on some systems).
|
Be sure to comment out the statement added in (9) before proceeding
if it produced a fatal run-time error!
E. Pointer Arithmetic
Another unusual thing about pointers is the way that addition and
subtraction work.
- Enter an output statement to display the values (intPtr2
- 1), intPtr2, and (intPtr2 + 1). (Remember:
these are addresses!) What values appear?
- What relationship can you observe between these three values?
- Next, modify your output statement to display the result of
dereferencing these three values (that is, *(intPtr2 - 1),
*intPtr2, and *(intPtr2 + 1)). What values appear?
- In the space below, explain what happened:
When an arithmetic operation such as addition, subtraction,
increment, or decrement is applied to the value in a pointer
variable, that value changes by a multiple of
sizeof(Type), where Type is the
type of value to which the pointer was defined to point. For
example, if sizeof(int) is 4, the expression
intPtr2++;
will add 4 to the value of intPtr2; and the expression
intPtr2--;
will subtract 4 from the value of intPtr2.
Choose one of these statements to try in pointers.cpp
and determine which variable intPtr2 points to following
the execution of that statement. Give your results in the space below:
In general, the expression
ptr += i
can be used to make ptr point to the address i x
sizeof(Type) past the original address.
|
F. Relational Operators
Pointers bound to the same type (or which are null) can be compared
with == and !=.
- Add the following statement to pointers.cpp:
dubPtr1 = dubPtr2 = &dub1;
Then add statements to determine the truth or falsity of the
following comparisons and record your results:
dubPtr1 == dubPtr2 _____________________
*dubPtr1 == *dubPtr2 _____________________
- Now add the statement:
dubPtr2 = &dub3;
and statements to determine the truth or falsity of the
following comparisons; record your results below:
dubPtr1 == dubPtr2 _____________________
*dubPtr1 == *dubPtr2 _____________________
|
Note the difference between ptr1 == ptr2 and
*ptr1 == *ptr2. The first compares two memory
addresses and the second compares the contents of
two memory locations.
|
G. Run-Time allocation
In practice, pointers are almost never used to store the addresses of
variables that have names because it is so much simpler to access the
value of the variable using its name, rather than by storing the
address of the variable in a pointer and then dereferencing it.
Instead, pointers are more typically used to store the addresses of
nameless (or anonymous) variables.
How can a variable have no name? The key is that the memory
associated with a normal variable is allocated at compile-time,
when the compiler encounters a declaration naming that variable:
Type variableName;
However, this is not the only way to allocate memory for a variable.
In particular, C++ provides an operator named new that can be
used to allocate memory at run-time. More precisely, the expression
allocates a block of memory large enough to hold an object of type
Type. The address of this block of memory is
the value produced by the new operator. Thus, if
intPtr is an int pointer variable (of type int
*) and we write
intPtr = new int;
then:
- a new block of (sizeof(int)) bytes is allocated;
- the new operator produces the address o fthat block of
memory as its value; and
- that address is then assigned to intPtr.
- Declare a double-pointer variable dubPtr
in pointers.cpp and use new to assign it a
value (an address). Display the address assigned and record
it in the space below:
Address assigned to dubPtr: _____________________
- In pointers.cpp, define a struct or class
Info containing a char member c, a
double member d, and an array x of
4 ints, and declare two Info-pointer
variables infoPtr1, infoPtr2, and infoPtr3
(that is, pointers to memory locations where values of type
Info can be stored). Then write three assignment
statements that use new to assign values (addresses)
to infoPtr1 and infoPtr2 and then display
these addresses. Record the values below:
Address assigned to infoPtr1: _____________________
Address assigned to infoPtr2: _____________________
Anything that can be done with a normal int variable can
now be done with this int-sized block of memory (even
though it has no name) whose address is assigned to
intPtr, thanks to our being able to dereference
intPtr. That is, if we write
*intPtr = 44; // Or cin >> *intPtr; and enter the value 44
then the value 44 is assigned to that memory location and
we can display this value using
cout << *intPtr << endl;
- Add statements to pointers.cpp to input a value
to store in the location pointed to by dubPtr and
then display the contents of this location. Write your
statements here and the output produced:
- Add statements to pointers.cpp to store the
values 'A', 3.1416, 1, 2, 3, and 4 in the
anonymous struct/class pointed to by infoPtr1 and
then display the value of this anonymous variable. Write
your statements below and the output produced.
Note: As detailed in Lab Exercise 8, parentheses
are needed in (*ptr).member because the dot operator
has higher priority than *; ptr->member is
a simpler but equivalent expression.
H. Run-Time Deallocation
When a block of memory allocated during run-time is no longer needed,
it should be returned to the "storage pool" of available memory
(usually called the free store or the heap). For this,
we use a statement of the form
Add statements to pointers.cpp to:
- Deallocate the memory allocated to infoPtr1;
- Allocate memory for infoPtr3;
- Display the address of the memory block allocated to infoPtr3.
Did the block of memory deallocated for infoPtr1 get
allocated to infoPtr3? _____________________
I. Run-Time Arrays
Blocks of memory like those we have been considering are called
anonymous variables because the compiler cannot associate a
name with them since they are allocated at run-time. In th eabsence
of a name, a pointer to such a block of memory provides us with a
means of accessing its value. However, anonymous integers, doubles,
and so on are far less convenient to deal with than named variables of
those types. As a result, new is almost never used to
allocate anonymous variables of one of the fundamental types.
Instead, new is used in one of two ways:
- To allocate arrays during run-time
- To allocate class/struct objects during run-time
Part 1 of Project 7 uses a run-time allocated array to store queue
elements; STL uses run-time allocated arrays (for vectors);
and run-time allocated class/struct objects are used in linked
structures (see Section 8.6 and Lab 8).
As a preview of (i), add the following statements to
pointers.cpp and describe what happens when they are
executed:
cout << "\n\Enter number of elements: ";
cin >> int1;
// Allocate a run-time array with
// int1 double elements.
dubPtr1 = new double[int1];
for (int i=0; i < int1; i++)
dubPtr1[i] = 1.1 * i;
for (int i=0; i < int1; i++)
cout << dubPtr1[i] << endl;
|
Results of executing this code segment |
Hand In: This lab handout with the answers filled in
attached to a listing of your final program
(use the enscript command from your Programming
Style Sheet to print it out:
enscript -E -G -2rj -M Letter -PECT2_PS pointers.cpp. Or,
you may use the a2ps command: a2ps pointers.cpp).
Ricky J. Sethi <rickys at sethi.org>
Last modified: Mon Apr 18 17:36:19 PDT 2005