2D Arrays in C


Two different approaches: Unlike Java, C allows two separate approaches to 2-dimensional arrays:
  1. Simulate a 2-dimensional array in sequential memory locations: An "ordinary" 2-dimensional array in C, declared perhaps as int a[5][4]; lays out all 20 4-byte storage locations in one sequence in memory -- row-by-row and then column-by-column. In the above case there are 5 rows and 4 columns in each row -- first rows, then columns. (Each row has length 4.) The 0th row is put into the first 4 sequential locations, followed immediately by the second row in the next 4 sequential locations, and so forth. A simple formula is used to get to the correct location in memory, given indexes i for the row and j for the column, that is, to get to an element a[i][j]. In this case, the formula is to skip over locations for i rows, and proceed an additional j locations, to get to location 4*i + j past the start of the array a. Thus in pointer arithmetic terms, the location a[i][j] is described by a + 4*i + j.

    Notice that C must know the length of each row (or what is the same thing, the number of columns) in order to locate an element. C does not need to know the number of rows.

    This layout of handling 2-dimensional arrays is the most efficient and uses the least space. It is not the safest way or the most flexible. Java does not support this type of 2-dimensional array.

  2. Use an array of pointers to 1-dimensional arrays: This is the only type of 2-dimensional array in Java. Here an array with 5 rows and 4 columns truly is an array of 5 pointers to 5 arrays, each with 4 elements. So there are 5 extra storage locations for the 5 pointers. To access an element requires an extra pointer chase, rather than a simple formula giving an offset from one address.

    This type of 2-dimensional array supports ragged arrays, which means that the rows don't all need to be the same length. Here there is an array of pointers to the rows, which can be any 1-dimentional array.


An example of each approach: The program on the left below uses an ordinary C 2-dimensional array, while the program on the right uses an array of pointers to 1-dimensional arrays.
"Ordinary" 2-d array of char Array of pointers to char
#include <stdio.h>

void p(int t[][4]) {
/* or "int t[5][4]",
"int (*t)[4]" */
int i, j;
for (i = 0; i < 5; i++)
switch (i) {
case 0:
for (j = 0; j < 5; j++) /* j==4 bad*/
printf("%4i", t[i][j]);
printf(" ");
break;
case 1:
for (j = 0; j < 5; j++)
printf("%4i", (*(t+i))[j]);
printf(" ");
break;
case 2:
for (j = 0; j < 5; j++)
printf("%4i", *(t[i] + j));
printf(" ");
break;
case 3:
for (j = 0; j < 5; j++)
printf("%4i", *(*(t + i) + j));
printf(" ");
break;
case 4:
for (j = 0; j < 5; j++)
printf("%4i", *(&t[0][0] + 4*i + j));
printf(" ");
break;
}
}

void q(int t[], int rowsize) { /* 1-d array */
int i;
for (i = 0; i < 20; i++) {
printf("%4i", t[i]);
if (((i+1)%rowsize) == 0) printf(" ");
}
}


int main(void) {
int s[5][4] = /* or "int s[][4];" */
{ { 0, 1, 2, 3},
{10, 11, 12, 13},
{20, 21, 22, 23},
{30, 31, 32, 33},
{40, 41, 42, 43}
};

p(s);
printf("******************* ");
q(s, 4); /* get compiler and lint warning */

}
#include <stdio.h>

void p(int *t[]) {
/* or "int **t", "int *t[5]",
"int *(t[5])", "int *(t[])" */
int i, j;
for (i = 0; i < 5; i++)
switch (i) {
case 0:
for (j = 0; j < 5; j++)
printf("%4i", t[i][j]);
printf(" ");
break;
case 1:
for (j = 0; j < 5; j++)
printf("%4i", (*(t+i))[j]);
printf(" ");
break;
case 2:
for (j = 0; j < 5; j++)
printf("%4i", *(t[i] + j));
printf(" ");
break;
case 3:
for (j = 0; j < 5; j++)
printf("%4i", *(*(t + i) + j));
printf(" ");
break;
case 4:
for (j = 0; j < 5; j++)
printf("%4i", t[i][j]);
printf(" ");
break;
/* Note: *(&t[0][0] + 4*i + j)
does not work here */

}
}







int main(void) {
int *s[5]; /* or "int *(s[5]);" */
int y[4] = {10, 11, 12, 13};
int z[4] = {20, 21, 22, 23};
int x[4] = { 0, 1, 2, 3};
int v[4] = {40, 41, 42, 43};
int u[4] = {30, 31, 32, 33};
s[0] = x; s[1] = y; s[2] = z;
s[3] = u; s[4] = v;

p(s);
}
% lint -m -u subs5.c
(53) warning: argument is incompatible with
prototype: arg #1

function returns value which is always ignored
printf
% cc -o subs5 subs5.c
"subs5.c", line 53: warning: argument is
incompatible with prototype: arg #1
% subs5
0 1 2 3 10
10 11 12 13 20
20 21 22 23 30
30 31 32 33 40
40 41 42 43-1073744680
*******************
0 1 2 3
10 11 12 13
20 21 22 23
30 31 32 33
40 41 42 43
% lint -m -u subs6.c

function returns value which is always ignored
printf
% cc -o subs6 subs6.c
% subs6
0 1 2 3 20
10 11 12 13-268436252
20 21 22 23 10
30 31 32 33 40
40 41 42 43 0

Commentary:
  1. In the program at the left, in order to calculate the address of a simple reference to location a[i][j] (where 0 <= i < 5 and 0 <= j < 4), the compiler starts with the address of a, and as mentioned before, skips over i rows of length 4 and then skips down an additional j locations, to get to a + 4*i + j.
  2. In contrast, the program on the right starts with the address of the array of pointers: a. It then skips down to the address of the ith pointer: a + i . The value of that ith pointer is given by *(a + i). This is the address of the ith 1-dimensional array, which is being pointed to. Then the address of the jth element in that array is *(a + i) + j. The actual jth element becomes *(*(a + i) + j).
  3. Remarkably enough, with one exception all possible array element designators will work with either program, as is demonstrated by the programs above. In each case, each row has 5 elements printed (including one element out of range for the array), where the printing uses 5 distinct methods. On the left, all five methods work, while on the right only the method described in item 1 above fails, since it assumes the array uses contiguous locations.
  4. The programs above print an additional item at the end of each row, using an array reference out of range (illegal in Java) which is just the next item in memory. The print of these values is given above in blue.

    The printout on the left is easy to understand, since each "extra" element is just the first element of the next row, as we would expect. For the printout on the right, realize that a compiler can store the arrays x, y, z, u, and v anywhere it wants. From the printout we see that in fact this compiler stored them in reverse order of their declaration: first the 4 locations for u, then the 4 locations for v, then x, then z, then y. At the end of y comes a garbage value.

  5. On the left it works to pretend that the array is just a 1-dimensional array of size 20. There is a compiler warning, but the 20 locations can be accessed sequentially. Of course this will not work on the right.
  6. In the program on the left above, the initialization of the 2-dimensional array could instead have had the form:
  7. I don't know of any fundamentally different way to initialize the 2-dimensional array in the program on the right.


Created By Dr. Neal Wager : The University of Texas at San Antonio