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, which then 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 the 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 in the addresses between the end of
iarray
and the start of carray
, as shown in this example.)
Constants are often used when defining the total capacity of an array rather than using a literal numeric value. Constants are aliases for C literal values, and are used instead of literals to make the code easier to read and to allow for it to be more easily updated. See C Constants to learn more about defining and using C constants. Here is an example defining and using a constant (
|
2.5.2. Two-Dimensional Arrays
C supports multidimensional arrays, but we limit our discussion of multidimensional 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 multidimensional array variable, specify the size of each dimension. For example:
int matrix[50][100];
short little[10][10];
Here, matrix
is a 2D array of int
values with 50 rows and 100 columns, and
little
is a 2D array of short
values 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.
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;
}
}
Two-Dimensional 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 multidimensional array parameters, you must indicate that the parameter is a multidimensional 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
* m: 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(void) {
int matrix[50][COLS];
int bigger[90][COLS];
init_matrix(matrix, 50);
init_matrix(bigger, 90);
...
Both the matrix
and the bigger
arrays 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. |
Two-Dimensional 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.
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:
-
Make a single call to
malloc
, allocating one large chunk of heap space to store all NxM array elements. -
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(void) {
int *two_d_array; // the type is a pointer to an int (the element type)
// allocate in a single malloc of N x M 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
.
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 of int
types allocated via a single malloc
is
a pointer to an int
, 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(void) {
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 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
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 (with the error detection and handling code 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 two_d_array
stores the address of a dynamically allocated array of
int
values (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
.
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(void) {
int **two_d_array;
// some code to allocate the row array and multiple col arrays
// ...
init2D_Method2(two_d_array, N, M);
...
Here, the function implementation can use double-indexing syntax. Unlike
statically declared 2D arrays, both the row and column dimensions need
to be passed as parameters: the rows
parameter specifies the bounds on
the outermost array (the array of row arrays), and the cols
parameter
specifies the bounds on the inner arrays (the array column values for each
row).