ECE 209 Arrays and Pointers

Initialization

int V[10] = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29 } ;
double X[100] = { 1.0e15, 3.7, 20 } ;
int squares[] = { 0, 1, 4, 9, 16, 25, 36, 49, 64, 81 } ;

Constant arrays

When an array is declared with const, its elements cannot be changed.

const int DaysOfMonth[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 } ;

Multi-dimensional arrays

Declaration

int D2[5][3] ;
double D3[2][4][6] ;

Meaning

If X is declared with int X[5][3] ; it is an array of 5 elements that are arrays of 3 elements that are integers.

Consequently, arrays are stored in row-major order. So, the first seven elements of X, as stored in memory, are X[0][0], X[0][1], X[0][2], X[1][0], X[1][1], X[1][2], and X[2][0].

Initialization on declaration

int A[3][2] = { { 2, 4}, {3, 9}, {4, 16 } } ;
int B[3][2] = { 2, 4, 3, 9, 4, 16 } ;

Initialization of identity matrix by for-loop

double M[1000][1000], i, j ;
for (i=0; i<1000; ++i) {
  for (j=0; j<1000; ++j) {
    if (i==j) {
      M[i][j] = 1.0 ;
    } else {
      M[i][j] = 0.0 ;
    }
  }
}

Arrays as arguments and pointer

Arrays as constants

The array variable itself is a constant, but you can change its elements.

If V is a one-dimensional array, assigning to V is not allowed but assigning to V[5] is allowed.

If X is a two-dimensional array, assigning to X[5] is not allowed but assigning to X[5][5] is allowed.

Arrays as pointers

C treats an array like a constant reference to its 0'th elements; that is, V and &V[0] are the same. For a two-dimensional array X, X[5] and &X[5][0] are the same.

Precedence and associativity

The postfix array index has a higher precedence that the prefix address operator. Therefore, &X[5][0] is &((X[5])[0]).

Attempts at generalization

Only good for an array of size 10

void reverse(int V[10]) {
  int i, j ;
  for (i=0, j=9; i<j; ++i, ++j) {
    int t ;
    t = V[i] ;
    V[i] = V[j] ;
    V[j] = t ;
  }
}

Only good for an array size fixed at compile time

#define SIZE 209

void reverse(int V[SIZE]) {
  int i, j ;
  for (i=0, j=SIZE-1; i<j; ++i, ++j) {
    int t ;
    t = V[i] ;
    V[i] = V[j] ;
    V[j] = t ;
  }
}

Good for an array of any size

void reverse(int V[], int n) {
  int i, j ;
  for (i=0, j=n-1; i<j; ++i, ++j) {
    int t ;
    t = V[i] ;
    V[i] = V[j] ;
    V[j] = t ;
  }
}

Problems with generalization

Let's try a generic identity function.

identity(int X[][], int n) ;

This is not allowed! Suppose identity was called with two different array sizes.

int Y[300][300] ;
int Z[400][400] ;

Y[1][0] is 300 integers past the start of Y. Z[1][0] is 400 integers past the start of Z. Since the compiler can't "see" the calls, it doesn't know what code to compile.

C99 solution

C99 provides for variable-length arrays or VLAs. The dimension of arrays can be set from expressions, though these dimensions cannot be changed once the array is declared.

int numStudents = 31 ;
char namesStudents[numStudents][16] ;

Variable length arrays can also be passed as parameters.

identity(int n, int X[n][n]) ;

ANSI C non-solution

ANSI C allows the safe passing of arrays if all but the first dimension is fixed.

identity(int X[][100]) ;

The complications of array/pointer declarations

int R[3][5] ; R is a 3×5 array R[0][0]
int *S[5] ; S is an array of 5 pointers to integers *S[0]
int (*T)[5] ; T is a pointer to an array of 5 integers (*T)[0]
int **V ; V is a pointer to a pointer to an integer **V
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv) {
  int R[3][5] ;
  int *S[5] ;
  int (*T)[5] ;
  int **V ;

  for(int i=0; i<3; ++i) {
    for (int j=0; j<5; ++j) {
      R[i][j] = i*10 + j ;
    }
  }

  for(int i=0; i<5; ++i) {
    S[i] = &R[i%3][i] ;
  }
  
  T = &R[1] ;

  V = &S[2] ;

  puts("\nValues of R[i][j]") ;
  for(int i=0; i<3; ++i) {
    for (int j=0; j<5; ++j) {
      printf("  R[%2d,%2d]:  %4d\n", i, j, R[i][j]) ;
    }
  }

  puts("\nValues of S[i]") ;
  for(int i=0; i<5; ++i) {
    printf("  S[%2d]:  %08x\n", i, (unsigned int)S[i]) ;
  }
  puts("Values of *S[i]") ;
  for(int i=0; i<5; ++i) {
    printf("  *S[%2d]:  %4d\n", i, *S[i]) ;
  }

  puts("\nValue of T") ;
  printf("  T:  %08x\n", (unsigned int)T) ;
  puts("Values of (*T)[i]") ;
  for(int i=0; i<5; ++i) {
    printf("  (*T)[%2d]:  %4d\n", i, (*T)[i]) ;
  }

  puts("\nValue of V") ;
  printf("  V:  %08x\n", (unsigned int)V) ;
  puts("Value of *V") ;
  printf("  *V:  %08x\n", (unsigned int)*V) ;
  puts("Value of **V") ;
  printf("  **V:  %4d\n", **V) ;

  return (EXIT_SUCCESS) ;
}

Values of R[i][j]
  R[ 0, 0]:     0
  R[ 0, 1]:     1
  R[ 0, 2]:     2
  R[ 0, 3]:     3
  R[ 0, 4]:     4
  R[ 1, 0]:    10
  R[ 1, 1]:    11
  R[ 1, 2]:    12
  R[ 1, 3]:    13
  R[ 1, 4]:    14
  R[ 2, 0]:    20
  R[ 2, 1]:    21
  R[ 2, 2]:    22
  R[ 2, 3]:    23
  R[ 2, 4]:    24

Values of S[i]
  S[ 0]:  bf8ebcc8
  S[ 1]:  bf8ebce0
  S[ 2]:  bf8ebcf8
  S[ 3]:  bf8ebcd4
  S[ 4]:  bf8ebcec
Values of *S[i]
  *S[ 0]:     0
  *S[ 1]:    11
  *S[ 2]:    22
  *S[ 3]:     3
  *S[ 4]:    14

Value of T
  T:  bf8ebcdc
Values of (*T)[i]
  (*T)[ 0]:    10
  (*T)[ 1]:    11
  (*T)[ 2]:    12
  (*T)[ 3]:    13
  (*T)[ 4]:    14

Value of V
  V:  bf8ebcbc
Value of *V
  *V:  bf8ebcf8
Value of **V
  **V:    22

Pointer arithmetic

If p is an integer pointer (which could just be the first element of an array), what is p[i]?

If p is an integer pointer and i is an integer, p+i is the address of the i'th integer after the one p is addressing.

So p[i] is *(p+i).

C code once thought to be fast

reverse(int *p, int *q) {
  if (p>q) {
    int *t ;
    t = p ;
    p = q ;
    q = t ;
  }
  while (p<q) {
    int t ;
    t  = *p ;
    *p = *q ;
    *q = t ;
    ++p ;
    --q ;
  }
}

It is unlikely that p++ adds one to the bits stored in p. It adds the size of an integer.

Take care with assignments

  int *p ;
  double *q ;
q = p ; illegal
*q = *p ; allowed, *p is made into a double
q = (double *)p ; allowed, but probably not a good idea

You didn't see this here...

Let's try to get identity to work in a unsavory manner. Just pass in the address of the first element of the two-dimensional array along with the size of its dimensions.

identity(&X[0][0], n) ;

Now do some really ugly pointer manipulation. Remember that every n+1 elements must be set equal to 1. Also n+1×n-1 is n2-1.

identity(int *p, int n) {
  int i, j ;
  for (i=0; i<n-1; ++i) {
    *p = 1 ;
    ++p ;
    for (j=0; j<n; ++j) {
      *p = 0 ;
      ++p ;
    }
  }
  *p = 1 ;
}

Or even slightly more obscurely.

identity(int *p, int n) {
  int i, j ;
  for (i=0; i<n-1; ++i) {
    *(p++) = 1 ;
    for (j=0; j<n; ++j)
      *(p++) = 0 ;
  }
  *p = 1 ;
}

read-only arrays

If a function does not modify an array, this should be clearly indicated in the prototype.

int sum(const int a[], int n) ;
double DotProduct(const double *X, const double *Y, int n) ;

constant vigilence

int * * const R ; Prevents assignment to R
int * const * R ; Prevents assignment to *R
const int * * R ; Prevents assignment to **R

constant warnings

Take a look at that example on pages 383 of the textbook (C Primer Plus by Stephen Prata).

int * p1;
const int * p2 ;
const int ** pp2 ;
p1 = p2 ;     /* GCC warning:  Assignment discard qualifiers */
p2 = p1 ;
pp2 = &p1 ;   /* GCC warning:  Assignment from incompatiable type */

The first assignment is forbidden because it would allow the *p2 to be modified as *p1. The second assignment is allowed because the original p2 is "lost". Assigning to *p1 cannot modify the original *p2 values.

The third assignment is also forbidden. So, why is assigning a int * to a const int * allowed, but assigning a int ** to a const int ** not allowed?

The reason is seen in this modified example.

int * p1;
const int ** pp2 ;
const int n = 13 ;
pp2 = &p1 ;   /* GCC warning:  Assignment from incompatiable type */
*pp2 = &n ;
*p1 = 10 ;

If the assignment were allowed, the constant n could be modified. Rather than attempt to understand these complex rules, it might be better to never to assign to anything with a const qualifier anywhere in its declaration.

lack of constancy

The following looks like a useful function. Can it ever be used to modify its const arguments.

void ArraySum(int *A, const int *B, const int *C, int n) {
  int i ;
  for (i=0; i<n; ++i) {
    A[i] = B[i] + C[i] ;
  }
}

It could, if the arrays overlap.

  int X[20] ;
  X[0] = 1 ;
  ArraySum(&X[1], X, X, 19) ;

This possibility of aliasing prevents the compiler from performing several additions of the loop concurrently.

In C99 (and C++) declaring a pointer with the restrict qualifier informs the compiler that an object is allowed to have only one name. If ArraySum were declared with restricted pointers, the result of the aliased execution of ArraySum would be undefined.

Determining the size of an array

Often array sizes depend on constants.

One file may define the constants.

#define NUMCARS 35

Declarations will use the constants to set aside memory for data.

double mpg[NUMCARS] ;
int wheels[NUMCARS] ;

Loops will also use the constants to do the work.

double avgMPG ;
avgMPG = 0.0 ;
for (i=0; i<NUMCARS; ++i) {
  avgMPG += mpg[i] ;
}
avgMPG = avgMPG/N ;

However don't be surprised to see obscure uses of sizeof to determine the number of elements within an array.

const int elementsInV = sizeof(V)/sizeof(V[0]) ;

Examples -- if time permits

// generate a histogram from an array argument
void genHistogram(const int V[], int vSize, int H[], int hSize) ;

// add the elements of an array
int sumElements(const int V[], int vSize) ;

// find the largest element in an array
int maxElement(const int V[], int vSize) ;

// count the number times a value occurs within an array)
int countValue(const int V[], int vSize, int e) ;