I l@ve RuBoard Previous Section Next Section

1.6 Pointers Allow for Flexibility

Our display solution in the preceding section has two primary drawbacks. First, it has a fixed upper limit of six sequences; if the user should guess all six sequences, the program unexpectedly terminates. Second, it always displays the same six element pairs in the same order. How might we extend our program's flexibility?

One possible solution is to maintain six vectors, one for each sequence, calculated to some number of elements. With each iteration of the loop, we draw our element pair from a different vector. When using a vector a second time, we draw our element pair from a different index within the vector. This approach resolves both drawbacks.

As with our earlier solution, we'd like to access the different vectors transparently. In the preceding section, we achieve transparency by accessing each element by index rather than by name. With each loop iteration, we increment the index value by 3. Otherwise, the code remains invariant.

In this section, we achieve transparency by accessing each vector indirectly by a pointer rather than by name. A pointer introduces a level of indirection to a program. Rather than manipulate an object directly, we manipulate a pointer that holds the address of an object. In our program, we define a pointer that can address a vector of integers. With each loop iteration, we modify the pointer to address a different vector. The actual code that manipulates the pointer does not change.

The use of a pointer does two things to our program. It increases the program's flexibility and adds a level of complexity absent in direct object manipulation. This section should convince you of the truth of both statements.

We already know how to define an object. The following statement, for example, defines ival as an object of type int initialized to a value of 1,024:



int ival = 1024; 

A pointer holds the address of an object of a particular type. To define a pointer of a particular type, we follow the type name with an asterisk:



int *pi; // pi is a pointer to an object of type int 

pi is a pointer to an object of type int. How do we initialize it to point to ival? The evaluation of an object's name, such as



ival; // evaluates to the value of ival 

evaluates to its associated value ?1,024 in this case. To retrieve the address of the object rather than its value, we apply the address-of operator (&):



&ival; // evaluates to the address of ival 

To initialize pi to ival's address, we write the following:



int *pi = &ival; 

To access the object addressed by a pointer, we must dereference the pointer ?that is, retrieve the object sitting at the address held by the pointer. To do that, we apply an asterisk to the pointer as follows:



// dereference pi to access the object it addresses 


if ( *pi != 1024 )  // read 


     *pi = 1024;    // write 

The initial complexity of using a pointer, as you can see, comes from its confusing syntax. The complexity in this case stems from the dual nature of a pointer: Either we can manipulate the address contained by the pointer, or we can manipulate the object to which the pointer points. When we write



pi; // evaluates to the address held by pi 

we are, in effect, manipulating the pointer object. When we write



*pi; // evaluates to the value of the object addressed by pi 

we are manipulating the object pi addresses.

A second complexity introduced by a pointer is the possibility that it addresses no object. For example, when we write *pi, this may or may not cause our program to fail at run-time! If pi addresses an object, our dereference of pi works exactly right. If pi addresses no object, however, our attempt to dereference pi results in undefined run-time behavior. This means that when we use a pointer, we must be sure that it addresses an object before we attempt to dereference it. How do we do that?

A pointer that addresses no object has an address value of 0 (it is sometimes called a null pointer). Any pointer type can be initialized or assigned a value of 0.



// initialize each pointer to address no object 





int *pi = 0; 


double *pd = 0; 


string *ps = 0; 

To guard against dereferencing a null pointer, we test a pointer to see whether its address value is zero. For example,



if ( pi && *pi != 1024 ) 


     *pi = 1024; 

The expression



if ( pi && ... ) 

evaluates to true only if pi contains an address other than 0. If it is false, the AND operator does not evaluate its second expression. To test whether a pointer is null, we typically use the logical NOT operator:



if ( ! pi ) // true if pi is set to 0 

Here are our six vector sequence objects:



vector<int> fibonacci, lucas, pell, triangular, square, pentagonal; 

What does a pointer to a vector of integer objects look like? Well, in general, a pointer has this form:



type_of_object_pointed_to * name_of_pointer_object 

Our pointer addresses the type vector<int>. Let's name it pv and initialize it to 0:



vector<int> *pv = 0; 

pv can address each of the sequence vectors in turn. Of course, we can assign pv the address of an explicit sequence:



pv = &fibonacci; 


// ... 


pv = &lucas; 

But doing the assignment this way sacrifices code transparency. An alternative solution is to store the address of each sequence within a vector. This technique allows us to access them transparently through an index:



const int seq_cnt = 6; 





// an array of seq_cnt pointers to 


//    objects of type vector<int> 


vector<int> *seq_addrs[ seq_cnt ] = { 


   &fibonacci,  &lucas, &pell, 


   &triangular, &square, &pentagonal 


}; 

seq_addrs is a built-in array of elements of type vector<int>*. seq_addrs[0] holds the address of the fibonacci vector, seq_addrs[1] holds the address of the lucas vector, and so on. We use this to access the individual vectors through an index rather than by name:



vector<int> *current_vec = 0; 


// ... 





for ( int ix = 0; ix < seq_cnt; ++ix ) 


{ 


      current_vec = seq_addrs[ ix ]; 


      // all element display is implemented 


      // indirectly through current_vec 


} 

The remaining problem with this implementation is that it is totally predictable. The sequence is always Fibonacci, Lucas, Pell, and so on. We'd like to randomize the display order of our sequences. We can do that using the C language standard library rand() and srand() functions:



#include <cstdlib> 





srand( seq_cnt ); 


seq_index = rand() % seq_cnt; 


current_vec = seq_addrs[ seq_index ]; 

rand() and srand() are standard library functions that support pseudo-random number generation. srand() seeds the generator with its parameter. Each call of rand() returns an integer value in a range between 0 and the maximum integer value an int can represent. We must clamp that value between 0 and 5 to have it be a valid index into seq_addrs. The remainder (%) operator ensures that our index is between 0 and 5. The cstdlib header file contains the declaration of both functions.

We handle a pointer to a class object slightly differently than we handle a pointer to an object of a built-in type. This is because a class object has an associated set of operations that we may wish to invoke. For example, to check whether the first element of the fibonacci vector is set to 1, we might write



if ( ! fibonacci.empty() && 


     ( fibonacci[1] == 1 )) 

How would we achieve the same tests indirectly through pv? The dot connecting fibonacci and empty() is called a member selection operator. It is used to select class operations through an object of the class. To select a class operation through a pointer, we use the arrow member selection operator (->):



! pv->empty() 

Because a pointer can address no object, before we invoke empty() through pv we must first check that pv's address is nonzero:



pv && ! pv->empty() 

Finally, to apply the subscript operator, we must dereference pv. (The additional parentheses around the dereference of pv are necessary because of the higher precedence of the subscript operator.)



if ( pv && ! pv->empty() && (( *pv )[1] == 1 )) 

We look at pointers again in the discussion of the Standard Template Library in Chapter 3, and in Chapter 6, in which we design and implement a binary tree class. For a more in-depth discussion of pointers, refer to Section 3.3 of [LIPPMAN98].

    I l@ve RuBoard Previous Section Next Section