new, two-dimensional, multi-dimensionalC++ 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)auto, automatic type deductionThe 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.
#include <iostream>
#include <iomanip>
using namespace std;
int main()
{
int nrows = 15;
const int ncols = 10;
auto scores = new int[nrows][ncols];
for (int i = 0; i < nrows; i++)
for (int j = 0; j < ncols; j++)
scores[i][j] = i * j;
for (int i = 0; i < nrows; i++)
{
for (int j = 0; j < ncols; j++)
cout << setw(5) << scores[i][j];
cout << endl;
}
return 0;
}
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 dynamically create multi-dimensional arrays with new. For an n-dimensional array, the first n-1 dimensions store pointers to the next dimension; the array referenced by the last dimension is the only one that stores data. 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 the ideas first introduced in the context of two-dimensional arrays to three dimensions:
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 the view that most closely matches a given problem or is 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 it does require 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 that differs from the storage order of the initializer list. 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 that they must synthesize three dimensions from a single dimension using an indexing function (pink). Although it is based on 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.