C++ doesn't limit the number of array dimensions programs can create and use. Creating arrays with two or more dimensions as automatic or local variables on the stack is straightforward. However, it is surprisingly more difficult to create them dynamically on the heap with the new
operator. Some seemingly natural ways of dynamic array allocation do not work.
Dimensions | Automatic (Local, Stack) | Dynamic (Heap) |
---|---|---|
1 | int scores[15]; |
int* scores = new int[15]; |
(a) | (b) | |
2 | int scores[15][10]; |
int* scores = new int[15][10]; // (1) int** scores = new int[15][10]; // (2) int* scores[50] = new int[15][10]; // (3) |
(c) | (d) - Syntax Errors | |
2 | int** scores = new int* [nrows] { new int[ncols] }; int** scores = (int **) new int[nrows][ncols]; int** scores = reinterpret_cast<int **>(new int[nrows][ncols]); |
|
(e) - Runtime Errors |
new
operator as a heap variable.new
operator returns a single pointer, seemingly justifying defining scores as a single-dimensional pointer variable, but compilation fails with the diagnostic: "cannot convert from 'int (*)[10]' to 'int *'."The failures of (d) and (e) notwithstanding, programs can create multi-dimensional arrays dynamically. The following discussion presents three solutions:
delete
- the Java approach)The ANSI standard's 2011 extension of the auto keyword affords a partial solution for the multi-dimensional dynamic array problem. While the solution is limited, its simplicity makes it the favored approach, if the program can function within the limitation.
Java takes a different approach to creating multi-dimensional arrays: it creates arrays of arrays. Specifically, creating a two-dimensional array begins by creating a one-dimensional array whose elements are one-dimensional arrays. C++ programs can take the same approach with two advantages: Programmers can specify each dimension size with a variable (rather than a compile-time constant), and the indexing or element access notation is straightforward. However, it has two disadvantages: Creating the arrays is a complex, multi-step process, and the program must deallocate the arrays to avoid a memory leak.
The textbook first introduced arrays-of-arrays as a way to return two-dimensional arrays from functions with the return
operator. But programmers can also use this "trick" to create multi-dimensional arrays dynamically with new
. Only the last dimension's array saves data when extended to three and higher dimensions, while the other dimensional arrays contain pointers. The advantages and disadvantages continue in higher dimensions, but the disadvantages become more pronounced with each additional dimension.
#include <iostream> #include <iomanip> using namespace std; int main() { int nrows; int ncols; cout << "Number of rows: "; cin >> nrows; cout << "Number of columns: "; cin >> ncols; int** table = new int* [nrows]; // (1) for (int i = 0; i < nrows; i++) // (2) table[i] = new int[ncols]; for (int row = 1; row <= nrows; row++) for (int col = 1; col <= ncols; col++) table[row-1][col-1] = row * col; // (3) for (int row = 0; row < nrows; row++) { for (int col = 0; col < ncols; col++) cout << setw(4) << table[row][col]; // (3) cout << endl; } for (int i = 0; i < nrows; i++) // (4) delete[] table[i]; delete[] table; return 0; } |
|
(a) | (b) |
int**
, meaning that it is a pointer to a pointer. The data type int*
means that each element of table is a pointer to an integer, that is, an array of integers.The following discussion extends to three dimensions ideas first introduced in the context of two-dimensional arrays:
In the case of three-dimensional arrays, the "natural" initialization order and rows×columns×layers view are not the same. The difference forces programmers to choose which view most closely matches a given problem or is the easiest to program. While they can create different row-major ordering operations for each view, the operations may not always follow the general formula.
Example Program | Initialization Order | Console Output |
---|---|---|
#include <iostream> #include <iomanip> using namespace std; int main() { char array[4][3][2] = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X' }; for (int i = 0; i < 4; i++) for (int j = 0; j < 3; j++) { for (int k = 0; k < 2; k++) cout << setw(2) << array[i][j][k]; cout << endl; } return 0; } |
A B C D E F G H I J K L M N O P Q R S T U V W X |
|
(a) | (b) | (c) |
Example Program | Console Output |
---|---|
#include <iostream> #include <iomanip> using namespace std; inline int index(int i, int j, int k, int ncols, int nlayr) { return k + nlayr * (j + ncols * i); } int main() { int nrows = 4; int ncols = 3; int nlayr = 2; char* array = new char[nrows * ncols * nlayr] { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X' }; cout << index(1, 0, 1, ncols, nlayr) << " -> " << array[index(1, 0, 1, ncols, nlayr)] << endl; cout << index(3, 0, 1, ncols, nlayr) << " -> " << array[index(3, 0, 1, ncols, nlayr)] << endl; return 0; } |
7 -> H 19 -> T |
new
operator. Although Figure 1 demonstrates that new
fails when creating multi-dimensional arrays, this example succeeds by synthesizing multiple dimensions from a single dimension (blue) and a row-major indexing function (pink). The program calls the indexing function to access a specific array element, mapping from the problem's three dimensions (specified by i, j, and k) to the array's linear single dimension (gold).
Specifying the size of each dimension with a variable allows programs to read or calculate the sizes while they run. The program implements the indexing function, index, as an inline function function for efficiency. It doesn't depend on the number of rows but requires the number of columns and layers. So, function calls require five arguments, making them somewhat cumbersome but also making them flexible - it works with any three-dimensional array. The cout statements locate the elements representing H and T (shaded in blue in the previous figure) using the ordering function.
Typically, programs solving problems with three-dimensional arrays read or calculate the values saved in each array element rather than taking them from an initializer list. This situation allows programmers to choose a mental or abstract array view different from the initializer list storage order. The following examples use a series of assignment operations, three per line for compactness and to match the row-first storage order, but would be part of the problem solution in a more authentic program.
Example Program | Rows×Cols×Layers | Console Output |
---|---|---|
#include <iostream> #include <iomanip> using namespace std; int main() { char [4][3][2]; array[0][0][0] = 'A'; array[0][1][0] = 'B'; array[0][2][0] = 'C'; array[1][0][0] = 'D'; array[1][1][0] = 'E'; array[1][2][0] = 'F'; array[2][0][0] = 'G'; array[2][1][0] = 'H'; array[2][2][0] = 'I'; array[3][0][0] = 'J'; array[3][1][0] = 'K'; array[3][2][0] = 'L'; array[0][0][1] = 'M'; array[0][1][1] = 'N'; array[0][2][1] = 'O'; array[1][0][1] = 'P'; array[1][1][1] = 'Q'; array[1][2][1] = 'R'; array[2][0][1] = 'S'; array[2][1][1] = 'T'; array[2][2][1] = 'U'; array[3][0][1] = 'V'; array[3][1][1] = 'W'; array[3][2][1] = 'X'; for (int k = 0; k < 2; k++) for (int i = 0; i < 4; i++) { for (int j = 0; j < 3; j++) cout << setw(2) << array[i][j][k]; cout << endl; } return 0; } |
A B C D E F G H I J K L M N O P Q R S T U V W X |
|
(a) | (b) | (c) |
Example Program | Console Output |
---|---|
#include <iostream> #include <iomanip> using namespace std; inline int index(int i, int j, int k, int nrows, int ncols) { return nrows * ncols * k + (j + ncols * i); } int main() { int nrows = 4; int ncols = 3; int nlayr = 2; char* array = new char[nrows * ncols * nlayr] { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X' }; cout << index(2, 1, 0, nrows, ncols) << " -> " << array[index(2, 1, 0, nrows, ncols)] << endl; cout << index(2, 1, 1, nrows, ncols) << " -> " << array[index(2, 1, 1, nrows, ncols)] << endl; return 0; } |
7 -> H 19 -> T |
new
. This technique allows programmers to specify the number of dimensions and sizes with runtime variables (blue). The cost of these benefits is they must synthesize three dimensions from one with an indexing function (pink). Although based on the row-major ordering, the function does not follow the general formula: it requires the number of rows and columns. To locate an element in layer k, it must skip all elements in the layers <k, calculated by nrows*ncols*k.