Name: _____________________ Class: CET 375
SSN/ID:   _____________________ Section & Group: ____________
Pointers and Dynamic Allocation/Deallocation


Introduction

As was noted in Lab Exercise 4, a variable data object can be thought of as having three components:

  1. The variable's name is the way we normally refer to it in a program.
  2. 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).
  3. 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

&variable

produces the address of the memory location in which a variable named variable is stored.

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

Type * pointerName;

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, ...;

    1. In the space below, declare two int pointer variables named intPtr1 and intPtr2 and two double pointer variables named dubPtr1 and dubPtr2.










    2. 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: ______________________
      	
    3. 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.

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










    2. Is the value of intPtr2 the same as the address of int2 that you recorded in part A? ______________________
    3. Is the value of dubPtr2 the same as the address of dub2 that you recorded in part A? ______________________

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










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

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

  2. One last thing about assignment; in pointers.cpp:
    1. Declare an int pointer intPtr3 and assign it the value 0.
    2. Declare a double pointer dubPtr3 and assign it the value 0.
    3. 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).
  1. 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:

    1. The value of intPtr2 is accessed, which is an address (currently of int2); and
    2. 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.

    1. Now add the statement
      
      	*intPtr2 = 99;
      	
      to pointers.cpp and then display the value of int1, int2, and int3. What change has occurred?










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

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

    1. Enter an output statement to display the values (intPtr2 - 1), intPtr2, and (intPtr2 + 1). (Remember: these are addresses!) What values appear?










    2. What relationship can you observe between these three values?










    3. 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?










    4. 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.
  1. 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 !=.

    1. 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   _____________________
      	
    2. 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

new Type

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:
  1. a new block of (sizeof(int)) bytes is allocated;
  2. the new operator produces the address o fthat block of memory as its value; and
  3. that address is then assigned to intPtr.
    1. 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:  _____________________
      	
    2. 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;
        
    1. 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:














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

delete pointerVariable

  1. Add statements to pointers.cpp to:
    1. Deallocate the memory allocated to infoPtr1;
    2. Allocate memory for infoPtr3;
    3. 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:

  1. To allocate arrays during run-time
  2. 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).

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