Chapter 14: Understanding Arrays and Pointers

C pointers and arrays are closely related; so closely, in fact, that they are often interchangeable! However, this does not mean pointers and arrays are identical. We will explore this relationship and why we might want to use either array notation or pointer notation to access array elements. Most often, we will need to understand this relationship when we pass arrays to functions. So, understanding the interchangeability of arrays and pointers will be essential to making any function that manipulates arrays more flexible and expressive.

Having read two previous chapters, Chapter 11Working with Arrays, and Chapter 13Using Pointers, is essential to understanding the concepts presented here. Please do not skip those chapters before reading this one.

The following topics will be covered in this chapter:

  • Reviewing the memory layout of arrays
  • Understanding the relationship between array names and pointers
  • Using pointers in various ways to access and traverse arrays
  • Expanding the use of pointer arithmetic, specifically for array elements
  • Creating an array of pointers to arrays and a two-dimensional (2D) array to highlight their differences

Technical requirements

Continue to use the tools you chose from the Technical requirements section of Chapter 1Running Hello, World!.

The source code for this chapter can be found at https://github.com/PacktPublishing/Learn-C-Programming-Second-Edition/tree/main/Chapter14.

Understanding array names and pointers

As we have seen, elements of arrays can always be accessed via indices and traversed by means of an integer offset from the zeroth element. Sometimes, however, it is more convenient to access array elements via a pointer equivalent.

Let's begin by declaring an array and two pointers, as follows:

const int arraySize = 5;
int       array[5] = { 1 , 2 , 3 , 4 , 5 };
int*      pArray1  = NULL;
int*      pArray2  = NULL;

We have declared a contiguous block of arraySize, or 5, which are elements that are integers. We don't use arraySize in the array declaration because that would make the array a variable-length array (VLA) that cannot be initialized in this way. We have also declared two pointers to integers—pArray1 and pArray2. In the memory on my system, it looks something like this:

Figure 14.1 – Likely memory layout of pointers and array

Figure 14.1 – Likely memory layout of pointers and array

Remember that we can't control the ordering or location of variables, but we do know that arrays are guaranteed to be contiguous blocks of values beginning at the array's name.

The name of the array represents the location or address of its zeroth element. This should sound similar to pointers—in fact, it is. Without [ and ], the array name is treated just like a pointer. It is better to think of the array name as a special variable location that is the beginning of the array block. Because the array name alone is treated as an address, we can do the following:

pArray1 = array;

The value of pArray1 is now the address of the zeroth element of array[]

We can also be more explicit when we assign the address of the zeroth element, as follows:

pArray2 = &array[0];

Here, the target of pArray2 is the zeroth element of array[]. To be even more explicit, we could have written this with parentheses, as follows:

pArray2 = &(array[0]); 

Each of these assignments is functionally identical.

Because of precedence rules, parentheses are not needed; the operator precedence of [ ] is higher than & and is, therefore, evaluated first. array[0] evaluates to the specific array element with an offset of 0. Notice how array is treated as a pointer, even though it is a named location, but the array[0] array element is treated as a named location of a value or a variable that happens to be part of an array. Whenever [ n ] is given with the array name, it is best to think of a specific element within an array.

Now, array&array[0]pArray1, and pArray2 all point to exactly the same location. In the memory on my system, it looks something like this:

Figure 14.2 – Pointers pointing to the base of an array

Figure 14.2 – Pointers pointing to the base of an array

You may notice a bit of asymmetry here. pArray1 and pArray2 are the names of locations of pointer values, whereas array is the name of the beginning location of an integer array. This asymmetry vanishes when we think of the values of the pointer variables and the array name itself as the beginning address of the block of integers. The values of the pointer variables can change, but the address of the array name cannot. This will be clarified in the next section.

Understanding array elements and pointers

Individual elements of an array can be accessed either with array notation or via pointers.

We have already seen how to access the elements of array using array notation—[ and ], as illustrated in the following code snippet:

array[0] = 1;  // first element (zeroth offset)
array[1] = 2;
array[2] = 3;
array[3] = 4;
array[4] = 5;  // fifth element (fourth offset)

These statements assign 1..5 values to each element of our array, just as the single initialization statement did when we declared array[5].

Accessing array elements via pointers

Arithmetic can be performed with addresses. Therefore, we can access the elements of array using a pointer plus an offset, as follows:

*(pArray1 + 0) = 1;  // first element (zeroth offset)
*(pArray1 + 1) = 2;  // second element (first offset)
*(pArray1 + 2) = 3;  // third element (second offset)
*(pArray1 + 3) = 4;  // fourth element (third offset)
*(pArray1 + 4) = 5;  // fifth element (fourth offset)

Since pArray is a pointer, the * go-to address notation must be used to access the value at the address of that pointer. In the second through fifth elements, we must first add an offset value and then go to that computed address. Note that we must use ( and ) to properly calculate the address before assigning the value there. Also, note that *(pArray1 + 0) is identical to the abbreviated version, *pArray1.

You may have already noticed how adding an offset to a base address (pointer) is very similar to using an array name and an index. Now, we can see how array element referencing and pointer element referencing are equivalent, as follows:

                    array[0]            *(pArray1 + 0)
                    array[1]            *(pArray1 + 1)
                    array[2]            *(pArray1 + 2)
                    array[3]            *(pArray1 + 3)
                    array[4]            *(pArray1 + 4)

Notice how array is an unchanging address and pArray is also an address that does not change as offsets are added to it. The address of array is fixed and cannot be changed. However, even though the value of pArray can be changed—it is a variable, after all—in this example, the value of pArray is not changed. The address of each element is evaluated as an intermediate value—the unchanged value of pArray plus an offset. In the next section, we'll explore other ways of traversing an array by changing the pointer's value directly. 

Operations on arrays using pointers

Before this chapter, the only pointer operation we had used with arrays was assignment. Because we can perform simple arithmetic on pointers—addition and subtraction—these operations conveniently lend themselves to array traversal. 

Using pointer arithmetic

An integer value in pointer arithmetic represents the element to which the pointer points. When an integer is added to a pointer, the integer is automatically converted into the size of the pointer element in bytes and added to the pointer. This is equivalent to incrementing an array index. Put another way, incrementing a pointer is equivalent to incrementing the index of an array. 

Even though pointers are nearly identical to integers—they are positive, whole numbers that can be added, subtracted, and compared—they are treated slightly differently from integers when pointer arithmetic is performed. The following cases illustrate the various results when operations on a pointer and an integer are mixed:

pointer + integer → pointer

integer + pointer → pointer

pointer – integer → pointer

pointer – pointer → integer

When we add 1 to an integer variable, the value of the variable is increased by 1. However, when we add 1 to a pointer variable, the value of the pointer is increased by the sizeof() value of the type that the pointer points to. When a pointer points to, say, a double value and we increment the pointer by 1, we are adding 1 * sizeof(double), or 8 bytes, to the pointer value to point to the next double value. When a pointer points to a byte value and we increment the pointer by 1, we are adding 1 * sizeof(byte), or 1 byte. 

We can use pointer arithmetic in one of two ways—either by keeping the pointer value unchanged or by modifying the pointer value as we move through the array. In the first approach, we access the elements of array using a pointer plus an offset. We have already seen this in the previous section, as follows:

*(pArray1 + 0) = 1;  // first element (zeroth offset)
*(pArray1 + 1) = 2;  // second element (first offset)
*(pArray1 + 2) = 3;  // third element (second offset)
*(pArray1 + 3) = 4;  // fourth element (third offset)
*(pArray1 + 4) = 5;  // fifth element (fourth offset)

Throughout these accesses, the value of pArray1 does not change. Note that we must use ( and ) to properly calculate the address before accessing the value there. Also, note that *(pArray1 + 0) is identical to the abbreviated form, *pArray1.

In the second approach, we access the elements of array by first incrementing the pointer and then dereferencing the pointer value, as follows:

pArray1 = array;
            *pArray1 = 1; // first element (zeroth offset)
pArray +=1; *pArray1 = 2; // second element (first offset)
pArray +=1; *pArray1 = 3; // third element (second offset)
pArray +=1; *pArray1 = 4; // fourth element (third offset)
pArray +=1; *pArray1 = 5; // fifth element (fourth offset)

Because the increment of pArray is so closely associated with accessing its target, we have placed two statements on the same line, separating them with the end-of-statement (EOS; character. This method is a bit tedious since we have added four incremental assignment statements. 

Using the increment operator

Alternatively, we could modify the pointer value to sequentially access each element of the array using the increment operator. We can, therefore, eliminate the four extra incremental assignment statements and make our code a bit more concise, as follows:

*pArray1++ = 1;  // first element (zeroth offset)
*pArray1++ = 2;  // second element (first offset)
*pArray1++ = 3;  // third element (second offset)
*pArray1++ = 4;  // fourth element (third offset)
*pArray1   = 5;  // fifth element (fourth offset)

This is a very common C idiom that is used to access and increment the pointer in the same statement. The * operator has equal precedence with the unary operator, ++. When two operators have the same precedence, their order of evaluation is significant. In this case, the order of evaluation is left to right, so *pArray is evaluated first and the target of the pointer evaluation is then assigned a value. Because we use the ++ postfix, pArray is incremented after the value of pArray is used in the statement. In each statement, the pointer reference is accessed and then incremented to point to the next array element. Without ( ) precedence operations, we rely directly on C's precedence hierarchy.

Let's say we wanted to be a bit more obvious and chose to use the ( ) grouping operator. This grouping operator has higher precedence than both * and ++. What, then, would be the difference between (*pArray)++ and *(pArray++)

These provide two completely different outcomes. For (*pArray)++*pArray is dereferenced and its target is incremented. For *(pArray++)pArray is dereferenced, accessing its target, and then pArray is incremented.

Let's now see how we would use these array traversal techniques in loops, as follows:

#include <stdio.h>
int main( void )  {
  const int arraySize = 5;
  int  array[5] = { 1 , 2 , 3 , 4 , 5 };
  int* pArray1  = array;
  int* pArray2  = &(array[0]);
  
  printf("Pointer values (addresses) from initial assignments:

");
  printf( "      address of array = %p,    value at array = %d
" , array , *array );
  printf( "  address of &array[0] = %p, value at array[0] = %d
" , 
          &array[0] , array[0] );
  printf( "    address of pArray1 = %p,  value at pArray1 = %#lx
" , 
          &pArray1 , (unsigned long)pArray1 );
  printf( "    address of pArray2 = %p,  value at pArray2 = %p

" , 
          &pArray2 , pArray2 );

In this initial part of our program, we set up our array and create two pointers, both of which point to the first element of the array. Even though the syntax of each statement is slightly different, the end result is identical, as shown by the subsequent printf() statements. The first printf() statement clearly shows how an array name is directly interchangeable with a pointer. The remaining printf() statements show the addresses of each pointer variable and the value contained in each pointer variable.

Throughout the remainder of the program, the array is traversed using the following three methods:

  • Array indexing
  • Pointer plus incremented offset
  • Pointer incrementation

Pay particular attention to the array indexing and pointer incrementation methods here:

  printf( "
(1) Using array notation (index is incremented): 

" );
  for( int i = 0; i < arraySize ; i++ )  {
    printf( "  &(array[%1d]) = %p, array[%1d] = %1d, i++
", i , 
            &(array[i]), i , array[i] );
  }
  printf( "
(2) Using a pointer addition (offset is incremented): 

");
  for( int i = 0 ; i < arraySize; i++  )  {
    printf( "  pArray2+%1d = %p, *(pArray2+%1d) = %1d, i++
", i , 
            (pArray2+i) , i , *(pArray2+i) );
  }
  printf("
(3) Using pointer referencing (pointer is incremented):

");
  for( int i = 0 ; i < arraySize ; i++ , pArray1++ )  {
    printf( "  pArray1 = %p, *pArray1 = %1d, pArray1++
", 
            (unsigned long) pArray1 , *pArray1 );
  }
}

This program is formatted for brevity. Looking at it quickly may make you go a bit cross-eyed. The version of this program in the repository is formatted a bit more clearly.

Let's clarify some of the printf() format statements. %1d is used to print a decimal value constrained to a field of 1. It is critical to note here the subtle appearance of 1 (one) versus l (ell). %p is used to print a hexadecimal pointer value preceded by 0x. These format options will be more completely explored in Chapter 19Exploring Formatted Output. Lastly, because each for()… loop contains only one statement—the printf() statement—{ } is not needed for the for()… loop body and could have been omitted, but is there as a matter of good programming practice. 

Normally, this program would be formatted with whitespace to make each loop and printf() parameters stand out a bit more clearly. 

Create a new file called arrays_pointers.c. Type in the program and save your work. Build and run the program. You should see the following output in your terminal window:

Figure 14.3 – arrays_pointers.c output

Figure 14.3 – arrays_pointers.c output

Once you have got the program working, as in the preceding screenshot, you should take some time and experiment further with the program before moving on by doing the following:

  • Try removing the () grouping operator in various places. For instance, remove () from (pArray2+i) in the second loop. What happened to the pointer address? Why?
  • See what happens when you place ++ before the pointer or index (prefix incrementation).
  • As an added challenge, try traversing the array in reverse order, beginning at the last element of the array, and decrementing the offsets/indices in each of the three methods. As you make these changes, note which method is easier to manipulate. The repository has syarra_sretniop.c to show one way that this can be done. However, this is just one example; your version may be very different.

As you perform your experiments, pay attention to which method may be easier to modify, clearer to read, or simpler to execute.

Passing arrays as function pointers revisited

We can now understand how array names and pointers to arrays are passed in function arguments. If arrays were passed by values in a function parameter, the entire array might then be copied into the function body. This is extremely inefficient, especially for very large arrays. However, the array itself is not passed by a value; the reference to it is copied. That reference is then used within the function to access array elements. This reference is actually a pointer value—an address.

So, the array values themselves are not copied, only the address of the zeroth element. C converts the array named location (without []) to a pointer value, &array[0], and uses that to access array from within the function body.

Interchangeability of array names and pointers

The real power of being able to interchange an array name with a pointer is when we use an array name (or a pointer to an array) as a parameter to be passed into a function. In this section, we will explore the four possible ways of using an array in a function parameter, as follows:

  • Pass an array name in a parameter and then use array notation in the function.
  • Pass a pointer to an array in a parameter and then use the pointer in the function.
  • Pass an array name in a parameter and use that as a pointer in the function.
  • Pass a pointer to an array in a parameter and use array notation in the function.

The third and fourth methods should now come as no surprise to you. In the arrays_pointers_funcs.c program, we'll create function prototypes for each of the functions, set up a simple array that we want to traverse, print out some information about the array's address, and then call each of the four functions in turn. For each function, the intention is that the output will be identical to the others. The code is illustrated in the following snippet:

#include <stdio.h>

void traverse1( int size , int  arr[] );

void traverse2( int size , int* pArr );

void traverse3( int size , int  arr[] );

void traverse4( int size , int* pArr );

int main(int argc, char *argv[])  {

  const int arraySize = 5;

  int  array[5] = { 10 , 20 , 30 , 40 , 50 };

  

  printf("Pointer values (addresses) from initial assignments: ");

  

  printf( "      address of array = %p,    value at array = %d " ,

           array , *array );

  printf( "  address of &array[0] = %p, value at array[0] = %d " ,

          &array[0] , array[0] );

  traverse1( arraySize , array );

  traverse2( arraySize , array );

  traverse3( arraySize , array );

  traverse4( arraySize , array );

}

This is very similar to what we've already seen in the earlier program used in this chapter. The one thing that might be surprising here is how the functions are all called in the same way. Even though each of the four functions has a parameter that is of either the arr[] or *pArr type, the value that is passed to each is array. You now know that array is both the name of our array and, equivalently, a pointer to the first element of that array (the element with the s zeroth offset).

The first traversal is pretty much what we've seen already. An array name is passed in and array notation is used to print each value in a loop, as follows:

void traverse1( int size , int arr[] )  {
  printf( "
(1) Function parameter is an array, " );
  printf( "using array notation:

" );
  for( int i = 0; i < size ; i++ )  {
    printf( "  &(array[%1d]) = %p, array[%1d] = %1d, 
      i++
", i , &(arr[i]), i ,   arr[i] );
  }
}

In the second traversal, a pointer to the first element of the array is passed in. Again, using a loop, the array is traversed using that pointer, as follows:

 void traverse2( int size , int* pArr )  {
  printf( "
(2) Function parameter is a pointer, " );
  printf( "using pointer:

" );
  for( int i = 0 ; i < size ; i++ , pArr++ )  {
    printf( "  pArr = %p, *pArr = %1d, pArr++
", 
            pArr , *pArr );
  }
}

Notice the increment part of the for()… loop; using the , sequence operator, we see that i is incremented, as well as pArr. This is often a useful idiom to keep all incrementation in the increment expression of the for()… loop instead of putting the extra increment operation in the for()… loop body.

In the third traversal, an array name is passed in, but because of the interchangeability of arrays and pointers, we traverse the array using pointers. Again, the incrementation of the pointer is done in the increment expression of the for()… loop, as illustrated in the following code snippet:

void traverse3( int size , int arr[] )  {
  printf( "
(3) Function parameter is an array, " );
  printf( "using pointer:

" );
  for( int i = 0 ; i < size ; i++ , arr++ )  {
    printf( "  arr = %p, *arr = %1d, arr++
", 
             arr , *arr );
  }
} 

Finally, in the fourth traversal, a pointer to the first element of the array is passed in, and because of the interchangeability of pointers and arrays, we traverse the array using array notation with the pointer, as follows:

void traverse4( int size , int* pArr )  {
  printf( "
(4) Function parameter is a pointer, " );
  printf( "using array notation :

" );
  for( int i = 0; i < size ; i++ )  {
    printf( "  &(pArr[%1d]) = %p, pArr[%1d] = %1d, i++
", 
      i , &(pArr[i]), i ,   pArr[i] );
  }
}

Now, in your editor, create a new file called arrays_pointers_funcs.c, and type in the five code segments given here. Save, compile, and run your program. You should see the following output:

Figure 14.4 – arrays_pointers_funcs.c output

Figure 14.4 – arrays_pointers_funcs.c output

It should now be clear to you how array names and pointers are interchangeable. Use array notation or pointer access in whichever manner makes your code clearer and is consistent with the rest of your programs.

Introducing an array of pointers to arrays 

Before finishing this chapter, it is worth introducing the concept of an array of pointers to arrays. This may be thought of as an alternate 2D array. Such an array is somewhat different in memory than the standard arrays that we have so far encountered. Even though its memory representation is different, we access this alternate array in exactly the same way as we would a standard 2D array. Therefore, some caution is required when traversing the two kinds of arrays.

We declare a standard 2D array in the following way:

int arrayStd[3][5];

We have allocated a contiguous and inseparable block of 15 integers, which has 3 rows of 5 integers. Our conceptual memory picture of this is a single block referenced via a single name, arrayStd, as follows:

Figure 14.5 – A standard, contiguous 2D array

Figure 14.5 – A standard, contiguous 2D array

To declare an alternate 2D array using arrays of pointers, we would do the following:

int* arrayPtr[3] = {NULL}; 
...
int array1[5];
int array2[5];
int array3[5];
arrayPtr[0] = array1;
arrayPtr[1] = array2;
arrayPtr[2] = array3;
...

We first declare an array of three pointers to integers, arrayPtr. As a reminder, when working with pointers, it is always a good practice to initialize them to some known value as soon as possible; in this case, when the array is declared. Later, we declare array1array2, and array3, each of which holds five integers. We'll call them sub-arrays for the purposes of this discussion. Then, we assign the addresses of these arrays to arrayPtr. The conceptual memory picture of this group of arrays looks like this:

Figure 14.6 – Array of pointers to one-dimensional (1D) arrays

Figure 14.6 – Array of pointers to one-dimensional (1D) arrays

This is a very different memory layout. We have, in fact, created four arrays—one array of three pointers and three arrays of five integers each. Each array is a smaller, contiguous, and inseparable block. However, because these array declarations are separate statements, they are not necessarily all contiguous in memory. Each of the four arrays is a small contiguous block but, taken as a whole, they are not. Furthermore, there is no guarantee that they would all be contiguous, even though they've been declared consecutively.

Notice that there could be a lot of code in between the declaration of arrayPtr and the sub-arrays. We have declared these sub-arrays statically; that is, their size and time of declaration are known before the program runs. The other arrays could even be declared in a different function block and at various times. Alternatively, we could declare the sub-arrays dynamically, where their size and time of declaration are only known when the program is running. We will explore static and dynamic memory allocation in Chapter 17Understanding Memory Allocation and Lifetime, and Chapter 18Using Dynamic Memory Allocation.

Using array notation, we would access the third element of the second row or sub-array (remembering the one-offness of array offsets), as follows:

arrayStd[1][2];
arrayPtr[1][2];

Here is where things get interesting. Given that we have two very different in-memory structures, one a single contiguous block, and the other four smaller sub-blocks that are scattered, access to any element in either of them is identical using array notation, as illustrated in the following code snippet:

for( int i=0 ; i<3 ; i++ )
  for( int j=0 ; j<5 ; j++ )  {
     arrayStd[i][j] = (i*5) + j + 1;
     arrayPtr[i][j] = (i*5) + j + 1;
  }

That's great! However, if we use pointer arithmetic to traverse these arrays, we will need to use slightly different approaches. We assign the value of (i*5) + j + 1 to each array element. This calculation is significant; it shows the calculation the compiler makes to convert index notation into an element's address.

To access the standard array using pointers, we could simply start at the beginning element and iterate through the whole block because it is guaranteed to be contiguous, as follows:

int* pInteger = &(array[0][0]);
for( int i=0 ; i<3 ; i++ )  {
  for( int j=0 ; j<5 ; j++ ) {
     *pInteger = (i*5) + j + 1;
      pInteger++;
  }

Because array is a 2D array, we must get the address of the first element of this block; using just the array name will cause a type error. In this iteration, on a standard array using a pointer, i is used for each row and j is used for each column element. We saw this in Chapter 12Working with Multi-Dimensional Arrays.

To access the array of pointers to arrays, we must add an assignment to our iteration using a pointer, as follows:

for( int i=0 ; i<3 ; i++ )  {
  int* pInteger = arrayOfPtrs[i];
  for( int j=0 ; j<5 ; j++ ) {
     *pInteger = (i*5) + j + 1;
      pInteger++;
}

Because array1array2, and array3 have been declared in separate statements, we cannot be certain that they are actually adjacent to one another. This means that taken together, they are unlikely to be contiguous across all of them. Therefore, for each sub-array, we have to set the pointer to that beginning element and traverse the sub-array. Take note that this particular pointer traversal relies on the fact that we have declared each sub-array to be the same size.

Let's put this into a working program to verify our understanding. In this program, we'll declare these arrays, assigning them values at the declaration, and then traverse each with array notation and with pointers. The goal of the program is to show identical output for each traversal, as follows: 

#include <stdio.h>
int main( void)  {
    // Set everything up.
    // Standard 2D array.
  int arrayStd[3][5] = { { 11 , 12 , 13 , 14 , 15 } , 
                         { 21 , 22 , 23 , 24 , 25 } ,
                         { 31 , 32 , 33 , 34 , 35 } };
    // Array of pointers.  
  int* arrayPtr[3] = { NULL };
    // Array sizes and pointer for pointer traversal.
  const int rows = 3;
  const int cols = 5;  
  int* pInteger;  
    // Sub-arrays.
  int array1[5]     = { 11 , 12 , 13 , 14 , 15 };
  int array2[5]     = { 21 , 22 , 23 , 24 , 25 };
  int array3[5]     = { 31 , 32 , 33 , 34 , 35 };
  arrayPtr[0] = array1;
  arrayPtr[1] = array2;
  arrayPtr[2] = array3;

Both arrayStd and the sub-arrays are initialized upon array declaration. You can see that each row and its corresponding sub-array have the same values. Some other useful constants and a pointer variable are declared. These will be used in the traversals. First, we'll carry out the traversals of the two arrays using array notation. The array notation is the same for both and is illustrated in the following code snippet:

   // Do traversals.
  printf( "Print both arrays using array notation, 
    array[i][j].

");  
 
  for( int i = 0 ; i < rows ; i++ )  {
    for( int j = 0 ; j < cols ; j++ )  {
        printf( " %2d" , arrayStd[i][j]);
    }
    printf( "
" );
  }
  printf("
");
  
  for( int i = 0 ; i < rows ; i++ )  {
    for( int j = 0 ; j < cols ; j++ )  {
        printf( " %2d" , arrayPtr[i][j]);
    }
    printf( "
" );
  }
  printf("
");

Then, we'll carry out the traversals, using the temporary pointer we've already declared, as follows:

  printf( "Print both arrays using pointers, 
    *pInteger++.

");
  pInteger = &(arrayStd[0][0]);
  for( int i = 0 ; i < rows ; i++ )  {
    for( int j = 0 ; j < cols ; j++ )  {
      printf( " %2d" , *pInteger++);
    }
    printf( "
" );
  }
  printf("
");
  
  // Experiment: 
  //  First run with the symbol EXPERIMENT set to 0.
  //  Then rerun with EXPERIMENT set to 1.
  //  When EXPERIMENT == 0, the array will be correctly 
  //  traversed
  //  When EXPERIMENT == 1, the array will be incorrectly 
  //  traversed
  //    (traversed as if it were a contiguous 2D array)
#define EXPERIMENT 1
#if EXPERIMENT
  pInteger = arrayPtr[0]; // For experiment only.
#endif 
  
  for( int i = 0 ; i < rows ; i++ )  {
      //
      // See NOTE above. Sub-arrays may not be contiguous.
      //
#if !EXPERIMENT
    pInteger = arrayPtr[i];  // Get the pointer to 
                             // the correct sub-array.
#endif
    
    for( int j = 0 ; j < cols ; j++ )  {
      printf( " %2d" , *pInteger++);
    }
    printf( "
" );
  }
  printf("
");
}

In the traversal for arrayStd, the pointer is set to the first element and we use nested loops to iterate through each row and column, incrementing the pointer as we go. In the traversal for arrayPtr, note the added assignment that is needed for each row. This is the statement:

pInteger = arrayPtr[i];

The code snippet has an experiment to prove how arrayPtr is not a contiguous block. After you get the program working as intended, then you can perform this experiment to see what happens.

With your editor, create a program named arrayOfPointers.c and enter the three code segments given. Save the file, compile it, and run it. You should get the following output:

Figure 14.7 – Correctly traversing array of pointers to arrays

Figure 14.7 – Correctly traversing array of pointers to arrays

As you can easily see, each array traversal presents an identical output. Yay—success!

When you perform the experiment outlined in the last array traversal, you should see something like this:

Figure 14.8 – Incorrectly traversing array of pointers to arrays

Figure 14.8 – Incorrectly traversing array of pointers to arrays

Yay! This was also a success because we can clearly see what happens when pointers go awry. From this, we can conclude that using a pointer to traverse an array of pointers to sub-arrays is definitely not like using a pointer to traverse a 2D array. We get unexpected, odd values in the last two rows because printf() is interpreting the bytes that are not in those two sub-arrays. This also illustrates how we can access values outside of array bounds and that we will get bizarre results when we do—hence, test and verify. You now know exactly why this has been emphasized!

However, there are numerous advantages to this alternative array of sub-arrays, especially when each of the rows of the sub-arrays is not the same number of elements. Therefore, this is an important concept to master. We will reuse and expand on this concept in Chapter 15Working with Strings, Chapter 18Using Dynamic Memory Allocation, and Chapter 20Getting Input from the Command Line.

Summary

In this chapter, we used the concepts introduced in two earlier chapters, Chapter 11Working with Arrays, and Chapter 13Using Pointers, to learn how array names and pointers are related and interchangeable.

We have seen various memory layouts of arrays and pointers and used pointers in various ways to access and traverse arrays, both directly and via function parameters. We also have seen how pointer arithmetic pertains to elements of the array and is a bit different from integer arithmetic. We learned about some of the similarities and differences between a 2D array (a contiguous block of elements) and an array of pointers to sub-arrays (a collection of scattered arrays indexed by a single array of pointers). Finally, we looked at a set of simple programs that illustrate as well as prove the concepts we have learned.

Having explored arrays and pointers and their interrelationship, we are now ready to put all of these concepts to good use in the next chapter.

Questions

  1. Are array notation and pointers identical?
  2. When you increment an array index by 1, what does that do?
  3. When you increment a pointer to an array element by 1, what does that do?
  4. Is this array always contiguous: int arr[3][5]?
  5. Are the elements of an array of pointers to other arrays always contiguous?
  6. If we pass a pointer to an array as a function parameter, must we use pointer notation to access the elements of the array?
  7. Does C remember the size of any array?
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset