2.5. Arrays in C

In the previous chapter we introduced statically declared one-dimensional C arrays and discussed the semantics of passing arrays to functions. In the dynamic memory allocation section of this chapter, we introduced dynamically allocated one dimensional arrays and discussed the semantics of passing them to functions.

In this section, we take a more in-depth look at arrays in C. We describe both statically and dynamically allocated arrays in more detail and discuss two-dimensional arrays.

2.5.1. Single Dimensional Arrays

Statically Allocated

Before jumping into new content, we briefly summarize static arrays with an example. See the previous chapter for more detail on statically declared one-dimensional arrays.

Statically declared arrays are allocated either on the stack (for local variables) or in the data region of memory (for global variables). A programmer can declare an array variable by specifying its type (the type stored at each index), and its total capacity (number of elements).

When passing an array to a function, C copies the value of the base address to the parameter. That is, both the parameter and the argument refer to the same memory locations — the parameter pointer points to the argument’s array elements in memory. As a result, modifying the values stored in the array through an array parameter modifies the values stored in the argument array.

Here are some examples of static array declaration and use:

// declare arrays specifying their type and total capacity
float averages[30];   // array of float, 30 elements
char  name[20];       // array of char, 20 elements
int i;

// access array elements
for (i = 0; i < 10; i++) {
    averages[i] = 0.0 + i;
    name[i] = 'a' + i;
}
name[10] = '\0';    // name is being used for storing a C-style string

// prints: 3 d abcdefghij
printf("%g %c %s\n", averages[3], name[3], name);

strcpy(name, "Hello");
printf("%s\n", name);  // prints: Hello

Dynamically Allocated

In the Dynamic Memory Allocation section of this chapter, we introduced dynamically allocated one-dimensional arrays, including their access syntax and the syntax and semantics of passing dynamically allocated arrays to functions. Here, we present a short recap of that information with an example.

Calling the malloc function dynamically allocates an array on the heap at runtime. The address of the allocated heap space can be assigned to a global or local pointer variable that points to the first element of the array. To dynamically allocate space, pass malloc the total number of bytes to allocate for the array (using the sizeof operator to get the size of a specific type). A single call to malloc allocates a contiguous chunk of heap space of the requested size. For example:

// declare a pointer variable to point to allocated heap space
int    *p_array;
double *d_array;

// call malloc to allocate that appropriate number of bytes for the array

p_array = malloc(sizeof(int) * 50);      // allocate 50 ints
d_array = malloc(sizeof(double) * 100);  // allocate 100 doubles

// always CHECK RETURN VALUE of functions and HANDLE ERROR return values
if ( (p_array == NULL) || (d_array == NULL) ) {
    printf("ERROR: malloc failed!\n");
    exit(1);
}

// use [] notation to access array elements
for (i = 0; i < 50; i++) {
    p_array[i] = 0;
    d_array[i] = 0.0;
}

// free heap space when done using it
free(p_array);
p_array = NULL;

free(d_array);
d_array = NULL;

Array Memory Layout

Whether an array is statically declared or dynamically allocated via a single call to malloc, array elements represent contiguous memory locations (addresses):

 array [0]:  base address
 array [1]:  next address
 array [2]:  next address
   ...            ...
 array [99]: last address

The location of element i is at an offset i from the base address of the array. The exact address of the ith element depends on the number of bytes of the type stored in the array. For example, consider the following array declarations:

int  iarray[6];  // an array of six ints, each of which is four bytes
char carray[4];  // an array of four chars, each of which is one byte

The addresses of their individual array elements might look something like this:

 addr   element
 ----   -------
 1230:  iarray[0]
 1234:  iarray[1]
 1238:  iarray[2]
 1242:  iarray[3]
 1246:  iarray[4]
 1250:  iarray[5]
     ...
 1280:  carray[0]
 1281:  carray[1]
 1282:  carray[2]
 1283:  carray[3]

In this example 1230 is the base address of iarray and 1280 the base address of carray. Note that individual elements of each array are allocated to contiguous memory addresses: each element of iarray stores a 4-byte int value, so its element addresses differ by 4, and each element of carray stores a 1-byte char value, so its addresses differ by 1. There is no guarantee that the set of local variables are allocated to contiguous memory locations on the stack (hence, there could be a gap of addresses between the end of iarray and the start of carray, as shown in this example.)

2.5.2. Two Dimensional Arrays

C supports multi-dimensional arrays, but we limit our discussion of multi-dimensional arrays to two-dimensional (2D) arrays, since 1D and 2D arrays are the most commonly used by C programmers.

Statically Allocated 2D Arrays

To statically declare a multi-dimensional array variable, specify the size of each dimension. For example:

int   matrix[50][100];
short little[10][10];

matrix is a 2D array of ints with 50 rows and 100 columns, and little is a 2D array of shorts with 10 rows and 10 columns.

To access an individual element, indicate both the row and the column index:

int   val;
short num;

val = matrix[3][7];  // get int value in row 3, column 7 of matrix
num = little[8][4];  // get short value in row 8, column 4 of little

Figure 1 illustrates the 2D array as a matrix of integer values, where a specific element in the 2D array is indexed by row and column index values:

Accessing matrix[2][3] is like indexing into a grid at row 2 and column 3.
Figure 1. A two-dimensional array represented as a matrix. Accessing matrix[2][3] is like indexing into a grid at row 2 and column 3.

Programs often access the elements of a 2D array by iterating with nested loops. For example, the following nested loop initializes all elements in matrix to 0:

int i, j;

for (i = 0; i < 50; i++) {  // for each row i
    for (j = 0; j < 100; j++) { // iterate over each column element in row i
        matrix[i][j] = 0;
    }
}

2-D Array Parameters

The same rules for passing one-dimensional array arguments to functions apply to passing two-dimensional array arguments: the parameter gets the value of the base address of the 2D array (&arr[0][0]). In other words, the parameter points to the argument’s array elements and therefore the function can change values stored in the passed array.

For multi-dimensional array parameters, you must indicate that the parameter is a multi-dimensional array, but you can leave the size of the first dimension unspecified (for good generic design). The sizes of other dimensions must be fully specified so that the compiler can generate the correct offsets into the array. Here’s a 2D example:

// a C constant definition: COLS is defined to be the value 100
#define COLS  (100)

/*
 * init_matrix: initializes the passed matrix elements to the
 *              product of their index values
 *   matrix: a 2D array (the column dimension must be 100)
 *   rows: the number of rows in the matrix
 *   return: does not return a value
 */
void init_matrix(int m[][COLS], int rows) {
    int i, j;
    for (i = 0; i < rows; i++) {
        for (j = 0; j < COLS; j++) {
            m[i][j] = i*j;
        }
    }
}

int main() {
    int matrix[50][COLS];
    int bigger[90][COLS];

    init_matrix(matrix, 50);
    init_matrix(bigger, 90);
    ...

Both the matrix and the bigger array can be passed as arguments to the init_matrix function because they have the same column dimension as the parameter definition.

The column dimension must be specified in the parameter definition of a 2D array so that the compiler can calculate the offset from the base address of the 2D array to the start of a particular row of elements. The offset calculation follows from the layout of 2D arrays in memory.

2D Array Memory Layout

Statically allocated 2D arrays are arranged in memory in row-major order, meaning that all of row 0’s elements come first, followed by all of row 1’s elements, and so on. For example, given the following declaration of a 2D array of integers:

int arr[3][4];  // int array with 3 rows and 4 columns

Its layout in memory might look like Figure 2:

Declaring an array as "int arr[3][4]" yields three rows, each of which has four elements.  Row 0 consists of arr[0][0], arr[0][1], arr[0][2], and arr[0][3].  Row 1 consists of arr[1][0], arr[1][1], etc.
Figure 2. The layout of a two-dimensional array in row-major order.

Note that all array elements are allocated to contiguous memory addresses. That is, the base address of the 2D array is the memory address of the [0][0] element (&arr[0][0]), and subsequent elements are stored contiguously in row-major order (e.g., the entirety of row 1 is followed immediately by the entirety of row 2, and so on).

Dynamically Allocated 2D Arrays

Dynamically allocated 2D arrays can be allocated in two ways. For an NxM 2D array, either:

  1. Make a single call to malloc, allocating a single chunk of NxM heap space to store each array element.

  2. Make multiple calls to malloc, allocating an array of arrays. First, allocate a 1D array of N pointers to the element type, with a 1D array of pointers for each row in the 2D array. Then, allocate N 1D arrays of size M to store the set of column values for each row in the 2D array. Assign the addresses of each of these N arrays to the elements of the first array of N pointers.

The variable declarations, allocation code, and array element access syntax differ depending on which of these two methods a programmer chooses to use.

Method 1: Memory Efficient Allocation

In this method, a single call to malloc allocates the total number of bytes needed to store the NxM array of values. This method has the benefit of being more memory efficient because the entire space for all NxM elements will be allocated at once, in contiguous memory locations.

The call to malloc returns the starting address of the allocated space (the base address of the array), which (like a 1D array) should be stored in a pointer variable. In fact, there is no semantic difference between allocating a 1D or 2D array using this method: the call to malloc returns the starting address of a contiguously allocated chunk of heap memory of the requested number of bytes. Because allocation of a 2D array using this method looks just like allocation for a 1D array, the programmer has to explicitly map 2D row and column indexing on top of this single chunk of heap memory space (the compiler has no implicit notion of rows or columns and thus cannot interpret double indexing syntax into this malloc’ed space).

Here’s an example C code snippet that dynamically allocates a 2D array using method 1:

#define N 3
#define M 4

int main() {
    int *two_d_array;    // the type is a pointer to an int (the element type)

    // allocate in a single malloc of NxM int-sized elements:
    two_d_array = malloc(sizeof(int) * N * M);

    if (two_d_array == NULL) {
        printf("ERROR: malloc failed!\n");
        exit(1);
    }

    ...

Figure 3 shows an example of allocating a 2D array using this method and illustrates what memory might look like after the call to malloc:

We can allocate an array with malloc(sizeof(int) * (3*4)) and store the base address in a stack pointer variable.  Because malloc returns a contiguous chunk of memory, we can treat the memory as a collection of rows and columns in row-major order like a statically allocated array.
Figure 3. The results of allocating a 2D array with a single call to malloc.

Like 1D dynamically allocated arrays, the pointer variable for a 2D array is allocated on the stack. That pointer is then assigned the value returned by the call to malloc, which represents the base address of the contiguous chunk of NxM int storage locations in the heap memory.

Because this method uses a single chunk of malloc’ed space for the 2D array, the memory allocation is as efficient as possible (it only requires one call to malloc for the entire 2D array). It’s the more efficient way to access memory due to all elements being located close together in contiguous memory, with each access requiring only a single level of indirection from the pointer variable.

However, the C compiler does not know the difference between a 2D or 1D array allocation using this method. As a result, the double indexing syntax ([i][j]) of statically declared 2D arrays cannot be used when allocating a 2D array using this method. Instead, the programmer must explicitly compute the offset into the contiguous chunk of heap memory using a function of row and column index values ([i*M + j], where M is the column dimension).

Here’s an example of how a programmer would structure code to initialize all the elements of a 2D array:

// access using [] notation:
//   cannot use [i][j] syntax because the compiler has no idea where the
//   next row starts within this chunk of heap space, so the programmer
//   must explicitly add a function of row and column index values
//   (i*M+j) to map their 2D view of the space into the 1D chunk of memory
for (i = 0; i < N; i++) {
    for (j = 0; j < M; j++) {
        two_d_array[i*M + j] = 0;
    }
}
Method 1 (single Malloc) and function parameters

The base address of an array allocated via a single malloc is a pointer to an integer, so it can be passed to a function with an (int *) parameter. Additionally, the function must be passed row and column dimensions so that it can correctly compute offsets into the 2D array. For example:

/*
 * initialize all elements in a 2D array to 0
 *  arr: the array
 *  rows: number of rows
 *  cols: number of columns
 */
void init2D(int *arr, int rows, int cols) {
    int i, j;
    for (i = 0; i < rows; i++) {
        for (j = 0; j < cols; j++) {
            arr[i*cols + j] = 0;
        }
    }
}

int main() {
    int *array;
    array = malloc(sizeof(int) * N * M);
    if (array != NULL) {
        init2D(array, N, M);
    }
    ...

Method 2: The Programmer Friendly Way

The second method for dynamically allocating a 2D array stores the 2D array as an array of N 1D arrays (one 1D array per row). It requires N+1 calls to malloc: one malloc for the array of row arrays and one malloc each for each of the N row’s column arrays. As a result, the element locations within a row are contiguous, but elements are not contiguous across rows of the 2D array. Allocation and element access are not as efficient as in method 1, and the type definitions for variables can be a bit more confusing. However, using this method, a programmer can use double indexing syntax to access individual elements of the 2D array (the first index is an index into the array of rows, the second index is an index into the array of column elements within that row).

Here is an example of allocating a 2D array using method 2 (error detection and handling code have been removed for readability):

// the 2D array variable is declared to be `int **` (a pointer to an int *)
// a dynamically allocated array of dynamically allocated int arrays
// (a pointer to pointers to ints)
int **two_d_array;
int i;

// allocate an array of N pointers to ints
// malloc returns the address of this array (a pointer to (int *)'s)
two_d_array = malloc(sizeof(int *) * N);

// for each row, malloc space for its column elements and add it to
// the array of arrays
for (i = 0; i < N; i++) {
// malloc space for row i's M column elements
    two_d_array[i] = malloc(sizeof(int) * M);
}

In this example, note the types of the variables and the sizes passed to the calls to malloc. To refer to the dynamically allocated 2D array, the programmer declares a variable (two_d_array) of type int ** that will store the address of a dynamically allocated array of int * element values. Each element in the two_d_array stores the address of a dynamically allocated array of int (the type of two_d_array[i] is int *).

Figure 4 shows what memory might look like after the above example’s N+1 calls to malloc:

two_d_array is a stack variable that points to a dynamically allocated array of pointers.  Each of those pointers points to a 1D array of integers.
Figure 4. The arrangement of memory after allocating a 2D array with N+1 mallocs.

Note that when using this method, only the elements allocated as part of a single call to malloc are contiguous in memory. That is, elements within each row are contiguous, but elements from different rows (even neighboring rows) are not.

Once allocated, individual elements of the 2D array can be accessed using double indexing notation. The first index specifies an element in the outer array of int * pointers (which row), and the second index specifies an element in the inner int array (which column within the row).

int i, j;

for (i = 0; i < N; i++) {
    for (j = 0; j < M; j++) {
        two_d_array[i][j] = 0;
    }
}

To understand how double indexing is evaluated, consider the type and value of the following parts of the expression:

       two_d_array: an array of int pointers, it stores the base address of an
                 array of (int *) values. Its type is int** (a pointer to int *).

    two_d_array[i]: the ith index into the array of arrays, it stores an (int *)
                 value that represents the base address of an array of (int)
                 values.  Its type is int*.

 two_d_array[i][j]: the jth element pointed to by the ith element of the array of
                 arrays, it stores an int value, (the value in row i, column j
                 of the 2D array).  Its type is int.
Method 2 (an array of arrays) and function parameters

The array argument’s type is int ** (a pointer to a pointer to an int), and the function parameter matches its argument’s type. Additionally, row and column sizes should be passed to the function. Because this is a different type from method 1, both array types cannot use a common function (they are not the same C type).

Here’s an example function that takes a method 2 (array of arrays) 2D array as a parameter:

/*
 * initialize a 2D array
 * arr: the array
 * rows: number of rows
 * cols: number of columns
 */
void init2D_Method2(int **arr, int rows, int cols) {
    int i,j;

    for (i = 0; i < rows; i++) {
        for (j = 0; j < cols; j++) {
            arr[i][j] = 0;
        }
    }
}

/*
 * main: example of calling init2D_Method2
 */
int main() {
    int **two_d_array;

    // some code to allocate the row array and multiple col arrays
    // ...

    init2D_Method2(array, N, M);
    ...

Here, the function implementation can use double indexing syntax. Unlike statically declared 2D arrays though, both the row and column dimensions need to be passed as parameters: the rows parameter specifies the bounds on the outer-most array (the array of row arrays), and the cols parameter specifies the bounds on the inner-arrays (the array column values for each row).