Strictly speaking, there are no multidimensional arrays in C++. What are commonly referred to as multidimensional arrays are actually arrays of arrays. It can be helpful to keep this fact in mind when you use what appears to be a multidimensional array.
Exercises Section 3.5.5
Exercise 3.41: Write a program to initialize a
vector
from an array ofint
s.Exercise 3.42: Write a program to copy a
vector
ofint
s into an array ofint
s.
We define an array whose elements are arrays by providing two dimensions: the dimension of the array itself and the dimension of its elements:
int ia[3][4]; // array of size 3; each element is an array of ints of size 4
// array of size 10; each element is a 20-element array whose elements are arrays of 30 ints
int arr[10][20][30] = {0}; // initialize all elements to 0
As we saw in § 3.5.1 (p. 115), we can more easily understand these definitions by reading them from the inside out. We start with the name we’re defining (ia
) and see that ia
is an array of size 3. Continuing to look to the right, we see that the elements of ia
also have a dimension. Thus, the elements in ia
are themselves arrays of size 4. Looking left, we see that the type of those elements is int
. So, ia
is an array of size 3, each of whose elements is an array of four int
s.
We read the definition for arr
in the same way. First we see that arr
is an array of size 10. The elements of that array are themselves arrays of size 20. Each of those arrays has 30 elements that are of type int
. There is no limit on how many subscripts are used. That is, we can have an array whose elements are arrays of elements that are arrays, and so on.
In a two-dimensional array, the first dimension is usually referred to as the row and the second as the column.
As with any array, we can initialize the elements of a multidimensional array by providing a bracketed list of initializers. Multidimensional arrays may be initialized by specifying bracketed values for each row:
int ia[3][4] = { // three elements; each element is an array of size 4
{0, 1, 2, 3}, // initializers for the row indexed by 0
{4, 5, 6, 7}, // initializers for the row indexed by 1
{8, 9, 10, 11} // initializers for the row indexed by 2
};
The nested braces are optional. The following initialization is equivalent, although considerably less clear:
// equivalent initialization without the optional nested braces for each row
int ia[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};
As is the case for single-dimension arrays, elements may be left out of the initializer list. We can initialize only the first element of each row as follows:
// explicitly initialize only element 0 in each row
int ia[3][4] = {{ 0 }, { 4 }, { 8 }};
The remaining elements are value initialized in the same way as ordinary, single-dimension arrays (§ 3.5.1, p. 114). If the nested braces were omitted, the results would be very different. This code
// explicitly initialize row 0; the remaining elements are value initialized
int ix[3][4] = {0, 3, 6, 9};
initializes the elements of the first row. The remaining elements are initialized to 0.
As with any array, we can use a subscript to access the elements of a multidimensional array. To do so, we use a separate subscript for each dimension.
If an expression provides as many subscripts as there are dimensions, we get an element with the specified type. If we supply fewer subscripts than there are dimensions, then the result is the inner-array element at the specified index:
// assigns the first element of arr to the last element in the last row of ia
ia[2][3] = arr[0][0][0];
int (&row)[4] = ia[1]; // binds row to the second four-element array in ia
In the first example we supply indices for all the dimensions for both arrays. On the left-hand side, ia[2]
returns the last row in ia
. It does not fetch an element from that array but returns the array itself. We subscript that array, fetching element [3]
, which is the last element in that array.
Similarly, the right-hand operand has three dimensions. We first fetch the array at index 0
from the outermost array. The result of that operation is a (multidimensional) array of size 20. We take the first element from that 20-element array, yielding an array of size 30. We then fetch the first element from that array.
In the second example, we define row
as a reference to an array of four int
s. We bind that reference to the second row in ia
.
As another example, it is common to use a pair of nested for
loops to process the elements in a multidimensional array:
constexpr size_t rowCnt = 3, colCnt = 4;
int ia[rowCnt][colCnt]; // 12 uninitialized elements
// for each row
for (size_t i = 0; i != rowCnt; ++i) {
// for each column within the row
for (size_t j = 0; j != colCnt; ++j) {
// assign the element's positional index as its value
ia[i][j] = i * colCnt + j;
}
}
The outer for
loops through each of the array elements in ia
. The inner for
loops through the elements of those interior arrays. In this case, we set the value of each element as its index in the overall array.
for
with Multidimensional ArraysUnder the new standard we can simplify the previous loop by using a range for
:
size_t cnt = 0;
for (auto &row : ia) // for every element in the outer array
for (auto &col : row) { // for every element in the inner array
col = cnt; // give this element the next value
++cnt; // increment cnt
}
This loop gives the elements of ia
the same values as the previous loop, but this time we let the system manage the indices for us. We want to change the value of the elements, so we declare our control variables, row
and col
, as references (§ 3.2.3, p. 93). The first for
iterates through the elements in ia
. Those elements are arrays of size 4. Thus, the type of row
is a reference to an array of four int
s. The second for
iterates through one of those 4-element arrays. Hence, col
is int&
. On each iteration we assign the value of cnt
to the next element in ia
and increment cnt
.
In the previous example, we used references as our loop control variables because we wanted to change the elements in the array. However, there is a deeper reason for using references. As an example, consider the following loop:
for (const auto &row : ia) // for every element in the outer array
for (auto col : row) // for every element in the inner array
cout << col << endl;
This loop does not write to the elements, yet we still define the control variable of the outer loop as a reference. We do so in order to avoid the normal array to pointer conversion (§ 3.5.3, p. 117). Had we neglected the reference and written these loops as:
for (auto row : ia)
for (auto col : row)
our program would not compile. As before, the first for
iterates through ia
, whose elements are arrays of size 4. Because row
is not a reference, when the compiler initializes row
it will convert each array element (like any other object of array type) to a pointer to that array’s first element. As a result, in this loop the type of row
is int*
. The inner for
loop is illegal. Despite our intentions, that loop attempts to iterate over an int*
.
To use a multidimensional array in a range
for
, the loop control variable for all but the innermost array must be references.
As with any array, when we use the name of a multidimensional array, it is automatically converted to a pointer to the first element in the array.
When you define a pointer to a multidimensional array, remember that a multidimensional array is really an array of arrays.
Because a multidimensional array is really an array of arrays, the pointer type to which the array converts is a pointer to the first inner array:
int ia[3][4]; // array of size 3; each element is an array of ints of size 4
int (*p)[4] = ia; // p points to an array of four ints
p = &ia[2]; // p now points to the last element in ia
Applying the strategy from § 3.5.1 (p. 115), we start by noting that (*p)
says p
is a pointer. Looking right, we see that the object to which p
points has a dimension of size 4
, and looking left that the element type is int
. Hence, p
is a pointer to an array of four int
s.
The parentheses in this declaration are essential:
int *ip[4]; // array of pointers to int
int (*ip)[4]; // pointer to an array of four ints
With the advent of the new standard, we can often avoid having to write the type of a pointer into an array by using auto
or decltype
(§ 2.5.2, p. 68):
// print the value of each element in ia, with each inner array on its own line
// p points to an array of four ints
for (auto p = ia; p != ia + 3; ++p) {
// q points to the first element of an array of four ints; that is, q points to an int
for (auto q = *p; q != *p + 4; ++q)
cout << *q << ' ';
cout << endl;
}
The outer for
loop starts by initializing p
to point to the first array in ia
. That loop continues until we’ve processed all three rows in ia
. The increment, ++p
, has the effect of moving p
to point to the next row (i.e., the next element) in ia
.
The inner for
loop prints the values of the inner arrays. It starts by making q
point to the first element in the array to which p
points. The result of *p
is an array of four int
s. As usual, when we use an array, it is converted automatically to a pointer to its first element. The inner for
loop runs until we’ve processed every element in the inner array. To obtain a pointer just off the end of the inner array, we again dereference p
to get a pointer to the first element in that array. We then add 4 to that pointer to process the four elements in each inner array.
Of course, we can even more easily write this loop using the library begin
and end
functions (§ 3.5.3, p. 118):
// p points to the first array in ia
for (auto p = begin(ia); p != end(ia); ++p) {
// q points to the first element in an inner array
for (auto q = begin(*p); q != end(*p); ++q)
cout << *q << ' '; // prints the int value to which q points
cout << endl;
}
Here we let the library determine the end pointer, and we use auto
to avoid having to write the type returned from begin
. In the outer loop, that type is a pointer to an array of four int
s. In the inner loop, that type is a pointer to int
.
A type alias (§ 2.5.1, p. 67) can make it easier to read, write, and understand pointers to multidimensional arrays. For example:
using int_array = int[4]; // new style type alias declaration; see § 2.5.1 (p. 68)
typedef int int_array[4]; // equivalent typedef declaration; § 2.5.1 (p. 67)
// print the value of each element in ia, with each inner array on its own line
for (int_array *p = ia; p != ia + 3; ++p) {
for (int *q = *p; q != *p + 4; ++q)
cout << *q << ' ';
cout << endl;
}
Here we start by defining int_array
as a name for the type “array of four int
s.” We use that type name to define our loop control variable in the outer for
loop.
Exercises Section 3.6
Exercise 3.43: Write three different versions of a program to print the elements of
ia
. One version should use a rangefor
to manage the iteration, the other two should use an ordinaryfor
loop in one case using subscripts and in the other using pointers. In all three programs write all the types directly. That is, do not use a type alias,auto
, ordecltype
to simplify the code.Exercise 3.44: Rewrite the programs from the previous exercises using a type alias for the type of the loop control variables.
Exercise 3.45: Rewrite the programs again, this time using
auto
.