Wrox Press C++ Tutorial


Indirect Data Access

The variables that we have dealt with so far provide you with the ability to name a memory location in which you can store data of a particular type. The contents of a variable are either entered from an external source, such as the keyboard, or calculated from other values that are entered. There is another kind of variable in C++ which does not store data that you normally enter or calculate, but greatly extends the power and flexibility of your programs. This kind of variable is called a pointer.

What is a Pointer?

Each memory location that you use to store a data value has an address. The address provides the means for your PC hardware to reference a particular data item. A pointer is a variable that stores an address of another variable of a particular type. A pointer has a variable name just like any other variable and also has a type which designates what kind of variables its contents refer to. Note that the type of a pointer variable includes the fact that it's a pointer. A variable that is a pointer which can contain addresses of locations in memory containing values of type int, is of type 'pointer to int'.

Declaring Pointers

The declaration for a pointer is similar to that of an ordinary variable, except that the pointer name has an asterisk in front of it to indicate that it's a variable which is a pointer. For example, to declare a pointer pnumber that points to a variable of type long, you could use the following statement:

long* pnumber;

This declaration has been written with the asterisk close to the type name. If you wish, you can also write it as:

long *pnumber;

The compiler won't mind at all. However, remember that the type of the variable pnumber is 'pointer to long', which is often indicated by placing the asterisk close to the type name.

You can mix declarations of ordinary variables and pointers in the same statement. For example:

long* pnumber, number = 99;

This declares the pointer pnumber of type 'pointer to long' as before, and also declares the variable number, of type long. On balance, it's probably better to declare pointers separately from other variables, otherwise the statement can appear misleading as to the type of the variables declared, particularly if you prefer to place the * adjacent to the type name. The following statements certainly look clearer and putting declarations on separate lines enables you to add comments for them individually, making for a program that is easier to read:

// Declaration and initialization of long variable 
long number = 99; 
// Declaration of variable of type pointer to long
long* pnumber;

It's a common convention in C++ to use variable names beginning with p to denote pointers. This makes it easier to see which variables in a program are pointers, which in turn can make a program easier to follow.

Let's take an example to see how this works, without worrying about what it's for. We will come on to how this is used very shortly. Suppose we have the long integer variable number, as we declared it above containing the value 99. We also have the pointer, pnumber, of type pointer to long, which we could use to store the address of our variable number. But how can we obtain the address of a variable?

The Address-Of Operator

What we need is the address-of operator, &. This is a unary operator which obtains the address of a variable. It's also called the reference operator, for reasons we will discuss later in this chapter. To set up the pointer that we have just discussed, we could write this assignment statement:

pnumber = &number;            // Store address of number in pnumber

The result of this operation is illustrated below:

You can use the operator & to obtain the address of any variable, but you need a pointer of the same type to store it. If you want to store the address of a double variable for example, the pointer must have been declared as double*, which is type 'pointer to double'.

Using Pointers

Taking the address of a variable and storing it in a pointer is all very well, but the really interesting aspect is how you can use it. Fundamental to using a pointer is accessing the data value in the variable to which a pointer points. This is done using the indirection operator, *.

The Indirection Operator

The indirection operator, *, is used with a pointer to access the contents of the variable to which it points. The name 'indirection operator' stems from the fact that the data is accessed indirectly. It is also called the de-reference operator, and the process of accessing the data in the variable pointed to by a pointer is termed de-referencing the pointer.

One aspect of this operator that can seem confusing is the fact that we now have several different uses for the same symbol, *. It is the multiply operator, the indirection operator, and it is used in the declaration of a pointer. Each time you use *, the compiler is able to distinguish its meaning by the context. When you multiply two variables, A*B for instance, then there's no meaningful interpretation of this expression for anything other than a multiply operation.

Why Use Pointers?

A question that usually springs to mind at this point is, 'Why use pointers at all?' After all, taking the address of a variable you already know and sticking it in a pointer so that you can de-reference it seems like an overhead you can do without. There a several reasons why pointers are important.

First of all, as you will see shortly, you can use pointer notation to operate on data stored in an array, which often executes faster than if you use array notation. Secondly, when we get to define our own functions later in the book, you will see that pointers are used extensively for enabling access to large blocks of data, such as arrays within a function, that are defined outside. Thirdly and most importantly, you will also see later that you can allocate space for variables dynamically, that is, during program execution. This sort of capability allows your program to adjust its use of memory depending on the input to the program. Since you don't know in advance how many variables you are going to create dynamically, the only way you can do this is by using pointers - so make sure you get the hang of this bit.

Try It Out - Using Pointers

We can try out various aspects of pointer operations with an example:

//Ex3_05.cpp
// Exercising pointers
#include <iostream>
using namespace std;

int main()
{
   long* pnumber = NULL; // Pointer declaration & initialization
   long number1 = 55, number2 = 99;
   pnumber = &number1;   // Store address in pointer
   *pnumber += 11;       // Increment number1 by 11

   cout << endl
        << "number1 = " << number1
        << "   &number1 = " << hex << pnumber;

   pnumber = &number2;   // Change pointer to address of number2
   number1 = *pnumber*10; // 10 times number2

   cout << endl
        << "number1 = " << dec << number1
        << "   pnumber = " << hex << pnumber
        << "   *pnumber = " << dec << *pnumber;

   cout << endl;
   return 0;
}

On my computer, this example generates the following output:

How It Works

There is no input to this example. All operations are carried out with the initializing values for the variables. After storing the address of number1 in the pointer pnumber, the value of number1 is incremented indirectly through the pointer in this statement:

*pnumber += 11;                // Increment number1 by 11

Note that when we first declared the pointer pnumber, we initialized it to NULL. We'll discuss pointer initialization in the next section.

The indirection operator determines that we are adding 11 to the contents of the variable pointed to, that is, number1. If we forgot the *, we would be attempting to add 11 to the address stored in the pointer.

The values of number1 and the address of number1, stored in pnumber, are displayed. We use the hex manipulator to generate the address output in hexadecimal notation.

You can obtain the value of ordinary integer variables as hexadecimal output by using the manipulator hex. You send it to the output stream in the same way that we have applied endl, with the result that all following output will be in hexadecimal notation. If you want the following output to be decimal, you need to use the manipulator dec in the next output statement to switch the output back to decimal mode again.

After the first line of output, the contents of pnumber are set to the address of number2. The variable number1 is then changed to the value of 10 times number2:

number1 = *pnumber*10;                // 10 times number2

This is calculated by accessing the contents of number2 indirectly through the pointer. The second line of output shows the results of these calculations

The address values you see in your output may well be different from those shown in the screenshot above, since they reflect where the program is loaded in memory, which depends on how your operating system is configured. The 0x prefixing the address values indicates that they are hexadecimal numbers. Note that the addresses &number1 and pnumber (when it contains &number2) differ by four bytes. This shows that number1 and number2 occupy adjacent memory locations, as a long variable requires four bytes. The output demonstrates that everything is working as we expect.

Initializing Pointers

Just as with arrays, using pointers that aren't initialized is extremely hazardous. If you do this, you can overwrite random areas of memory. The resulting damage just depends on how unlucky you are, so it's more than just a good idea to initialize your pointers. It's very easy to initialize a pointer to the address of a variable that has already been defined. Here you can see that we have initialized the pointer pnumber with the address of the variable number just by using the operator & with the variable name:

int number = 0;                  // Initialized integer variable
int* pnumber = &number;          // Initialized pointer

When initializing a pointer with another variable, remember that the variable must already have been declared prior to the pointer declaration.

Of course, you may not want to initialize a pointer with the address of a specific variable when you declare it. In this case, you can initialize it with the pointer equivalent of zero. For this, Visual C++ provides the symbol NULL that is already defined as 0, so you can declare and initialize a pointer using the following statement, rather like we did in the last example:

int* pnumber = NULL;        // Pointer not pointing to anything

This ensures that the pointer doesn't contain an address that will be accepted as valid, and provides the pointer with a value that you can check in an if statement, such as:

if(pnumber == NULL)
   cout << endl << "pnumber is null.";

Of course, you can also initialize a pointer explicitly with 0, which will also ensure that it is assigned a value that doesn't point to anything. In spite of it being arguably somewhat less legible, if you expect to run your code with other compilers, it is preferable to use 0 as an initializing value for a pointer that you want to be null.

This is also more consistent with the current 'good practice' in C++, the argument being that if you have an object with a name in C++, it should have a type. However, NULL does not have a type - it's an alias for 0.

To use 0 as the initializing value for a pointer you would simply write:

int* pnumber = 0;                // Pointer not pointing to anything

To check whether a pointer contains a valid address, you could use the statement:

if(pnumber == NULL)              // or pnumber == 0
   cout << endl << "pnumber is null.";

Equally well, you could use the statement,

if(!pnumber)
   cout << endl << "pnumber is null.";

which does exactly the same as the previous example.

Of course, you can also use the form:

if(pnumber!= 0)
   // Pointer is valid, so do something useful

The address pointed to by the NULL pointer contains a junk value. You should never attempt to de-reference a null pointer.

Pointers to char

A pointer of type char* has the interesting property - it can be initialized with a string constant. For example, we can declare and initialize such a pointer with the statement:

char* proverb = "A miss is as good as a mile.";

This looks very similar to initializing a char array but it's slightly different. This will create the string constant (actually a constant array of type char) with the character string appearing between the quotes and terminated with /0, and store the address of the constant in the pointer proverb. This is shown in the figure below:

Try It Out - Lucky Stars With Pointers

We could rewrite our lucky stars example using pointers instead of an array to see how that would work:

// Ex3_06.cpp
// Initializing pointers with strings
#include <iostream>
using namespace std;

int main()
{
   char* pstr1 = "Robert Redford";
   char* pstr2 = "Hopalong Cassidy";
   char* pstr3 = "Lassie";
   char* pstr4 = "Slim Pickens";
   char* pstr5 = "Boris Karloff";
   char* pstr6 = "Oliver Hardy";
   char* pstr  = "Your lucky star is ";
   int dice = 0;

   cout << endl
        << " Pick a lucky star!"
        << " Enter a number between 1 and 6: ";
   cin >> dice;
   cout << endl;

   switch(dice)
   {
      case 1: cout << pstr << pstr1;
              break;
      case 2: cout << pstr << pstr2;
              break;
      case 3: cout << pstr << pstr3;
              break;
      case 4: cout << pstr << pstr4;
              break;
      case 5: cout << pstr << pstr5;
              break;
      case 6: cout << pstr << pstr6;
              break;
      default: cout << "Sorry, you haven't got a lucky star.";
   }

   cout << endl;
   return 0;
}

How It Works

The array in Ex3_04.cpp has been replaced by the six pointers, pstr1 to pstr6, each initialized with a name. We have also declared an additional pointer, pstr, initialized with the phrase that we want to use at the start of a normal output line. Because we have discrete pointers, it is easier to use a switch statement to select the appropriate output message than to use an if as we did in the original version. Any incorrect values that are entered are all taken care of by the default option of the switch.

Outputting the string pointed to by a pointer couldn't be easier. As you can see, you simply write the pointer name. It may cross your mind at this point that in Ex3_05.cpp we wrote a pointer name in the output statement and the address that it contained was displayed. Why is it different here? The answer lies in the way the output operation views a pointer of type 'pointer to char'. It treats a pointer of this type as a string (which is an array of char), and so outputs the string itself, rather than its address.

Using pointers has eliminated the waste of memory that occurred with the array version of this program, but the program seems a little long-winded now - there must be a better way. Indeed there is - using an array of pointers.

Try It Out - Arrays of Pointers

With an array of pointers of type char, each element can point to an independent string, and the lengths of each of the strings can be different. We can declare an array of pointers in the same way that we declare a normal array. Let's go straight to rewriting the previous example using a pointer array:

// Ex3_07.cpp
// Initializing pointers with strings
#include <iostream>
using namespace std;

int main()
{
   // Initializing a pointer array
   char* pstr[] =  { "Robert Redford", 
                     "Hopalong Cassidy",
                     "Lassie",
                     "Slim Pickens",
                     "Boris Karloff",
                     "Oliver Hardy"
                   };

   char* pstart = "Your lucky star is ";
   int dice = 0;

   cout << endl
        << " Pick a lucky star!"
        << " Enter a number between 1 and 6: ";
   cin >> dice;
   cout << endl;

   if(dice >= 1 && dice <= 6)           // Check input validity
      cout << pstart << pstr[dice-1];   // Output star name
   else
      // Invalid input
      cout << "Sorry, you haven't got a lucky star."; 

   cout << endl;
   return 0;
}

How It Works

In this case, we are nearly getting the best of all possible worlds. We have a one-dimensional array of char pointers declared such that the compiler works out what the dimension should be from the number of initializing strings. The memory usage that results from this is illustrated below:

Compared to using a 'normal' array, the pointer array carries less overhead in terms of space. With an array, we would need to make each row the length of the longest string, and six rows of seventeen bytes each is 102 bytes, so by using a pointer array we have saved a whole... -1 bytes! What's gone wrong? The simple truth is that for this small number of relatively short strings, the size of the extra array of pointers is significant. You would make savings if you were dealing with more strings that were longer and had more variable lengths.

Space saving isn't the only advantage that you get by using pointers. In a lot of circumstances you save time too. Think of what happens if you want to move 'Oliver Hardy' to the first position and 'Robert Redford' to the end. With the pointer array as above you just need to swap the pointers - the strings themselves stay where they are. If we had stored these simply as strings, as we did in Ex3_04.cpp, a great deal of copying would be necessary - we would need to copy the whole string 'Robert Redford' to a temporary location while we copied 'Oliver Hardy' in its place, and then we would need to copy 'Robert Redford' to the end position. This would require significantly more computer time to execute.

Since we are using pstr as the array name, the variable holding the start of the output message needs to be different, so we have called it pstart. We select the string that we want to output by means of a very simple if statement, similar to that in the original version of the example. We either display a star selection or a suitable message if the user enters an invalid value.

One weakness of the way we have written the program is that the code assumes there are six options, even though the compiler is allocating the space for the pointer array from the number of initializing strings that we supply. So if we add a string to the list, we have to alter other parts of the program to take account of this. It would be nice to be able to add strings and have the program automatically adapt to however many strings there are.

The sizeof Operator

A new operator will help us here. The sizeof operator produces an integer constant that gives the number of bytes occupied by its operand. For example, with the variable dice from the previous example, the statement,

cout << sizeof dice;

will output the value 4, since dice was declared as int and therefore occupies 4 bytes.

The sizeof operator can be applied to an element in an array or to the whole array. When the operator is applied to an array name by itself, it produces the number of bytes occupied by the whole array, whereas when it is applied to a single element with the appropriate index value or values, it results in the number of bytes occupied by that element. Thus, in the last example, we could output the number of elements in the pstr array with the expression:

cout << (sizeof pstr)/(sizeof pstr[0]);

The expression divides the number of bytes occupied by the whole pointer array by the number of bytes occupied by the first element of the array. Since all elements of the array occupy the same amount of memory, the result is the number of elements in the array.

Remember that pstr is an array of pointers - using the sizeof operator on the array or on individual elements will not tell us anything about the memory occupied by the text strings.

The sizeof operator can also be applied to a type name rather than a variable, in which case the result is the number of bytes occupied by a variable of that type. The type name should be enclosed between parentheses. For example, after executing the statement,

long_size = sizeof(long);

the variable long_size will have the value 4.

The result returned by sizeof is obviously an integer and you can always treat it as such. Its precise type is actually size_t, which is used for values measured in bytes. The type size_t, which will pop up in various contexts from time to time, is defined in various standard libraries in Visual C++ and is equivalent to unsigned int.

Try It Out - Using the sizeof Operator

We can use this to amend the last example so that it automatically adapts to an arbitrary number of string values from which to select:

// Ex3_08.cpp
// Flexible array management using sizeof
#include <iostream.h>
using namespace std;

int main()
{
   // Initializing a pointer array
   char* pstr[] = { "Robert Redford",
                    "Hopalong Cassidy",
                    "Lassie",
                    "Slim Pickens",
                    "Boris Karloff",
                    "Oliver Hardy"
                  };
   char* pstart = "Your lucky star is ";

   // Number of array elements
   int count = (sizeof pstr)/(sizeof pstr[0]);
   int dice = 0;
   cout << endl
        << " Pick a lucky star!"
        << " Enter a number between 1 and " << count << ": ";
   cin >> dice;
   cout << endl;

   if(dice >= 1 && dice <= count)       // Check input validity
      cout << pstart << pstr[dice-1];   // Output star name
   else
      // Invalid input
      cout << "Sorry, you haven't got a lucky star."; 

   cout << endl;
   return 0;
}

How It Works

As you can see, the changes required in the example are very simple. We just calculate the number of elements in the pointer array pstr and store the result in count. Then, wherever the total number of elements in the array was referenced as 6, we just use the variable count. You could now just add a few more names to the list of lucky stars and everything affected in the program will be adjusted automatically.

Constant Pointers and Pointers to Constants

The array pstr in the last example is clearly not intended to be modified in the program, and nor are the strings being pointed to, nor the variable count. It would be a good idea to ensure that these didn't get modified in error in the program. We could very easily protect the variable count from accidental modification by writing this:

const int count = (sizeof pstr)/(sizeof pstr[0]);

However, the array of pointers deserves closer examination. We declared the array like this:

// Initializing a pointer array
   char* pstr[] = { "Robert Redford",
                    "Hopalong Cassidy",
                    "Lassie",
                    "Slim Pickens",
                    "Boris Karloff",
                    "Oliver Hardy"
                  };

Each pointer in the array is initialized with the address of a string literal, "Robert Redford", "Hopalong Cassidy" and so on. VC6 adheres to the ANSI standard, and says that the type of a string literal is 'array of const char'. The reason the compiler allows you to assign a string literal to an array of char* rather than of const char* is for reasons of backwards compatibility with existing code. If you try to alter the character array with a statement like this,

*pstr[0] = 'X';

then the program will crash when you try to run it.

This was not the case with VC5, where the type of a string literal was 'array of char', and the line shown above would have happily let you re-christen Mr. Redford 'Xobert'.

We don't really want to have unexpected behavior like the program crashing at run time, and we can prevent it. A far better way of writing the declaration is as follows:

// Array of pointers to constants
   const char* pstr[] = { "Robert Redford",
                          "Hopalong Cassidy",
                          "Lassie",
                          "Slim Pickens",
                          "Boris Karloff",
                          "Oliver Hardy"
                        };

In this case, there is no ambiguity about the const-ness of the objects pointed to by the elements of the pointer array. If you now attempt to change these objects, the compiler will flag this as an error at compile time.

However, we could still legally write this statement:

pstr[0] = pstr[1];

Those lucky individuals due to be awarded Mr. Redford would get Mr. Cassidy instead, since both pointers now point to the same name. Note that this isn't changing the values of the objects pointed to by the pointer array element - it is changing the value of the pointer stored in pstr[0]. We should therefore inhibit this kind of change as well, since some people may reckon that good old Hoppy does not have the same sex appeal as Robert. We can do this with the following statement:

// Array of constant pointers to constants
   const char* const pstr[] = { "Robert Redford",
                                "Hopalong Cassidy",
                                "Lassie",
                                "Slim Pickens",
                                "Boris Karloff",
                                "Oliver Hardy"
                              };

To summarize, we can distinguish three situations relating to const, pointers and the objects to which they point:

In the first situation, the object pointed to cannot be modified, but we can set the pointer to point to something else:

const char* pstring = "Some text";

In the second, the address stored in the pointer can't be changed, but the object pointed to can be:

char* const pstring = "Some text";

Finally, in the third situation, both the pointer and the object pointed to have been defined as constant and, therefore, neither can be changed:

const char* const pstring = "Some text";

Of course, all this applies to pointers of any type. Type char is used here purely for illustrative purposes.

Pointers and Arrays

Array names can behave like pointers under some circumstances. In most situations, if you use the name of a one-dimensional array by itself, it is automatically converted to a pointer to the first element of the array. Note that this is not the case when the array name is used as the operand of the sizeof operator.

If we have these declarations,

double* pdata;
double data[5];

then we can write this assignment:

pdata = data;       // Initialize pointer with the array address

This statement assigns the address of the first element of the array data to the pointer pdata. Using the array name by itself refers to the address of the array, whereas the array name, data, with an index value refers to the contents of the element corresponding to that index value. So, if we want to store the address of that element in the pointer, then we have to use the address-of operator:

pdata = &data[1];

Here, the pointer pdata will contain the address of the second element of the array.

Pointer Arithmetic

You can perform arithmetic operations with pointers. You are limited to addition and subtraction in terms of arithmetic, but you can also perform comparisons using pointers to produce a logical result. Arithmetic with a pointer implicitly assumes that the pointer points to an array, and that the arithmetic operation is on the address contained in the pointer. For the pointer pdata, for example, we could assign the address of the third element of the array data to a pointer with this statement:

pdata = &data[2];

In this case, the expression pdata+1 would refer to the address of data[3], the fourth element of the data array, so we could make the pointer point to this element by writing this statement:

pdata += 1;          // Increment pdata to the next element

This increments the address contained in pdata by the number of bytes occupied by one element of the array, data. In general, the expression pdata+n, where n can be any expression resulting in an integer, will add n*sizeof(double) to the address contained in the pointer pdata, since it was declared to be of type pointer to double. This is illustrated below:

In other words, incrementing or decrementing a pointer works in terms of the type of the object pointed to. Increasing a pointer to long by one changes its contents to the next long address, and so increments the address by four. Similarly, incrementing a pointer to short by one will increment the address by two. The more common notation for incrementing a pointer is using the increment operator. For example, this,

pdata++;            // Increment pdata to the next element

is equivalent to (and more common than) the += form. However, the += form was used above to make it clear that while the increment value is actually specified as one, the effect is usually an increment greater than one, except in the case of a pointer to char.

The address resulting from an arithmetic operation on a pointer can be a value ranging from the address of the first element of the array to the address which is one beyond the last element. Outside of these limits, the behavior of the pointer is undefined.

You can, of course, de-reference a pointer on which you have performed arithmetic (there wouldn't be much point to it otherwise). For example, assuming that pdata is still pointing to data[2], then this statement,

*(pdata+1) = *(pdata+2);

is equivalent to this:

data[3] = data[4];

When you want to de-reference a pointer after incrementing the address it contains, the parentheses are necessary as the precedence of the indirection operator is higher than that of the arithmetic operators, + or -. If you write the expression *pdata+1, instead of *(pdata+1), this would add one to the value stored at the address contained in pdata, which is equivalent to executing data[2]+1. Since this isn't an lvalue, its use in the assignment statement above would cause the compiler to generate an error message.

We can use an array name as though it were a pointer for addressing elements of an array. If we have the same one-dimensional array as before, declared as

long data[5];

then using pointer notation, we can refer to the element data[3] for example as *(data+3). This kind of notation can be applied generally so that, corresponding to the elements data[0], data[1], data[2], we can write *data, *(data+1), *(data+2), and so on.

Try It Out - Array Names as Pointers

We could exercise this aspect of array addressing with a program to calculate prime numbers (a prime number is a number divisible only by itself and one).

// Ex3_09.cpp
// Calculating primes
#include <iostream>
#include <iomanip>
using namespace std;

int main()
{
   const int MAX = 100;        // Number of primes required
   long primes[MAX] = { 2,3,5 };  // First three primes defined
   long trial = 5;             // Candidate prime
   int count = 3;              // Count of primes found
   int found = 0;              // Indicates when a prime is found

   do
   {
      trial += 2;                    // Next value for checking
      found = 0;                     // Set found indicator

      // Try division by existing primes
      for(int i = 0; i < count; i++) 
      {
         found = (trial % *(primes+i)) == 0; // True for exact division                                                       
         if(found)               // If division is exact
            break;               // it's not a prime
      }

      if (found == 0)        // We got one... so save it in
         *(primes + count++) = trial;   // ... primes array
   }while(count < MAX);

   // Output primes 5 to a line
   for(int i = 0; i < MAX; i++)
   {
      if(i%5 == 0)      // New line on 1st, and every 5th line
         cout << endl;
      cout << setw(10) << *(primes+i);
   }

   cout << endl;
   return 0;
}

If you compile and execute this example you should get the output shown:

How It Works

We have the usual #include statements for iostream for input and output, and for iomanip since we will be using a stream manipulator to set the field width for output.

We use the constant MAX to define the number of primes that we want the program to produce. The primes array, which stores the results, has the first three primes already defined to start the process off. All the work is done in two loops, the outer do-while loop, which picks the next value to be checked and adds the value to the primes array if it is prime, and the inner for loop that actually checks the value to see whether it's prime or not.

The algorithm in the for loop is very simple and is based on the fact that if a number is not a prime, then it must be divisible by one of the primes found so far - all of which are less than the number in question, since all numbers are either prime or a product of primes. In fact, only division by primes less than or equal to the square root of the number in question need to be checked, so this example isn't as efficient as it might be.

found = (trial % *(primes+i)) == 0; // True for exact division

This statement sets the variable found to be 1 if there's no remainder from dividing the value in trial by the current prime, *(primes+i) (remember that this is equivalent to primes[i]), and 0 otherwise. The if statement causes the for loop to be terminated if found has the value 1, since the candidate in trial can't be a prime in that case.

After the for loop ends (for whatever reason), it's necessary to decide whether or not the value in trial was prime. This is indicated by the value in the indicator variable found.

*(primes + count++) = trial;   //    primes array

If trial does contain a prime, this statement stores the value in primes[count] and then increments count through the postfix increment operator.

Once MAX number of primes have been found, they are output with a field width of 10 characters, 5 to a line, as a result of this statement:


if(i%5 == 0)      // New line on 1st, and every 5th line
   cout << endl;

This starts a new line when i has the values 0, 5, 10, and so on.

Try It Out - Counting Characters Revisited

To see how handling strings works in pointer notation, we could produce a version of the program we looked at earlier for counting the characters in a string:

// Ex3_10.cpp
// Counting string characters using a pointer
#include <iostream>
using namespace std;

int main()
{
   const int MAX = 80;                 // Maximum array dimension
   char buffer[MAX];                   // Input buffer
   char* pbuffer=buffer;               // Pointer to array buffer
   cout << endl                        // Prompt for input
        << "Enter a string of less than "
        << MAX << " characters:"
        << endl;
   cin.getline(buffer, MAX, '\n');     // Read a string until \n

   while(*pbuffer)                     // Continue until \0
      pbuffer++;

   cout << endl
        << "The string \"" << buffer
        << "\" has " << pbuffer-buffer << " characters.";

   cout << endl;
   return 0;
}

How It Works

Here the program operates using the pointer pbuffer rather than the array name buffer. We don't need the count variable, since the pointer is incremented in the while loop until \0 is found. When the \0 is found, pbuffer will contain the address of that position in the string. The count of the number of characters in the string entered is therefore the difference between the address stored in the pointer pbuffer and the address of the beginning of the array denoted by buffer.

We could also have incremented the pointer in the loop by writing the loop like this:

while(*pbuffer++);                   // Continue until \0

Now the loop contains no statements, only the test condition. This would work adequately, except for the fact that the pointer would be incremented after \0 was encountered, so the address would be one more than the last position in the string. We would therefore need to express the count of the number of characters in the string as pbuffer-buffer-1.

Note that here we can't use the array name in the same way that we have used the pointer. The expression buffer++ is strictly illegal since you can't modify an array name - it isn't a pointer.

Using Pointers with Multidimensional Arrays

Using a pointer to store the address of a one-dimensional array is relatively straightforward, but with multidimensional arrays, things can get a little complicated. If you don't intend to do this, you can skip this section as it's a little obscure, but if your previous experience is with C, this section is worth a glance.

If you have to use a pointer with multidimensional arrays, you need to keep clear in your mind what is happening. By way of illustration, we can use an array beans, declared as follows:

double beans[3][4];

We can declare and assign a value to the pointer pbeans as follows:

double* pbeans;
pbeans = &beans[0][0];

Here we are setting the pointer to the address of the first element of the array, which is of type double. We could also set the pointer to the address of the first row in the array with the statement:

pbeans = beans[0];

This is equivalent to using the name of a one-dimensional array which is replaced by its address. We used this in the earlier discussion. However, because beans is a two-dimensional array, we cannot set an address in the pointer with the following statement:

pbeans = beans;           // Will cause an error!!

The problem is one of type. The type of the pointer you have defined is double*, but the array is of type double[3][4]. A pointer to store the address of this array must be of type double*[4]. C++ associates the dimensions of the array with its type and the statement above is only legal if the pointer has been declared with the dimension required. This is done with a slightly more complicated notation than we have seen so far:

double (*pbeans)[4];

The parentheses here are essential, otherwise you would be declaring an array of pointers. Now the previous statement is legal, but this pointer can only be used to store addresses of an array with the dimensions shown.

Pointer Notation with Multidimensional Arrays

You can use pointer notation with an array name to reference elements of the array. You can reference each element of the array beans that we declared earlier in two ways:

Therefore, the following two statements are equivalent:

beans[i][j]
*(*(beans+i)+j)

Let's look at how these work. The first line uses normal array indexing to refer to the element with offset j in row i of the array.

We can determine the meaning of the second line by working outwards from the inside. beans refers to the address of the first row of the array, so beans+i refers to row i of the array. The expression *(beans+i) is the address of the first element of row i, so *(beans+i)+j is the address of the element in row i with offset j. The whole expression therefore refers to the value of that element.

If you really want to be obscure - and it isn't recommended that you do so - the following two statements, where we have mixed array and pointer notation, are also legal references to the same element of the array:

*(beans[i]+j)
(*(beans+i))[j]

There is yet another aspect to the use of pointers which is really the most important of all: the ability to create variables dynamically. We will look into that next.


© 1998 Wrox Press