Computer memory occupies a linear address space, meaning that programs treat it as a huge, one-dimensional array. Therefore, if a program has an array with two or more dimensions, the compiler maps the array's indexes to a single memory index. Historically, compilers used two schemes to map multi-dimensional arrays to linear computer memory: row-major or column-major order. (However, the only well-known computer maker using column-major ordering switched to row-major long ago.) Row-major order maps the indexes of a two-dimensional array to a single, linear address by rows: left-to-right and top-to-bottom. Conceptually, the mapping process extends to higher dimensions, but the implementation becomes more complex.
array[i,j] = array + sizeof(T) * (i * ncols + j); |
|
(b) | |
array[i][j] = array[2][1] = 'H' memory[rm(i,j)] = memory[rm(2,1)] = memory[i * ncols + j] = memory[2 * 3 + 1] = memory[7] = 'H' | |
(a) | (c) |
T array[4][3]
, making nrows = 4 and ncols = 3. It fills the array elements with consecutive letters and labels the indexes to demonstrate the correspondence between the various representations. The example arbitrarily sets i and j (perhaps loop-control variables) to 2 and 1 and shows the elements' arrangement in computer memory for row- and column-major orderings. The mapping operation is independent of the array elements' type, so the example uses T as a general or unspecified type (i.e., a placeholder for a "real" type).Programmers must understand row-major ordering if they write programs initializing two-dimensional arrays with an initializer list, but we use two-dimensional arrays more often than initializer lists. Understanding row-major ordering also explains why C++ sometimes allows us to forgo the first dimension size. Although these reasons justify including row-major ordering in the text, we can also use them to solve some programming problems. The first example contrasts the "normal" array notation with a "trick" based on the row-major ordering operation.
#include <iostream> #include <iomanip> using namespace std; int main() { const int nrows = 4; const int ncols = 3; char array[nrows][ncols] = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L' }; for (int i = 0; i < nrows ; i++) { for (int j = 0; j < ncols; j++) cout << setw(2) << array[i][j]; cout << endl; } return 0; } |
#include <iostream> #include <iomanip> using namespace std; int main() { const int nrows = 4; const int ncols = 3; char array[nrows * ncols] = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L' }; for (int i = 0; i < nrows ; i++) { for (int j = 0; j < ncols; j++) cout << setw(2) << array[i * ncols + j]; cout << endl; } return 0; } |
(a) | (b) |
A B C D E F G H I J K L
new
operator. This technique allows programs to specify the array's size with a variable rather than a compile-time constant. Unfortunately, this flexibility doesn't extend to two or more dimensions. Fortunately, programmers can combine the row-major indexing "trick" demonstrated above with the new
operator to achieve the desired flexibility. However, the technique requires more effort to create and destroy the array, and it requires a mapping operation, which the following example implements with an inline function for efficiency.
Although multi-dimensioned array function arguments are independent of the dimension sizes, the corresponding function parameters are not (see Passing arrays as function arguments). Creating a general parameter, independent of the dimension sizes, is more challenging. Overloaded functions offer some relief but are practically limited to a small number of possible array configurations. Programmers can use the row-major ordering operation to implement a general function, albeit at the cost of a more complex and cumbersome function call.
Programmers can extend the row-major indexing function to handle both automatic (stack) and dynamic (heap) arrays. However, in the case of automatic arrays, the syntax can become complex and confusing.
Dynamic (Heap) | Automatic (Stack) |
---|---|
#include <iostream> using namespace std; inline int index(int row, int col, int ncols) { return row * ncols + col; } int main() { int nrows = 4; int ncols = 3; char* array = new char[nrows * ncols] { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L' }; cout << array[index(2, 1, 3)] << endl; return 0; } |
#include <iostream> using namespace std; inline int index(int row, int col, int ncols) { return row * ncols + col; } int main() { const int nrows = 4; const int ncols = 3; char array[4][3] = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L' } cout << *((char*)(array) + index(2, 1, 3)) << endl; return 0; } |