Multidimensional arrays in C++ are arrays of arrays. A two-dimensional array is really a one-dimensional block of memory with a fixed row width, and understanding that is what makes the rest — indexing, passing to functions, and the surprising difference between a row pointer and a row array — fall into place.
Declaring a grid
A two-dimensional array declares a number of rows and a number of columns. Both must be constant expressions.
#include
int main() {
int shelf[3][4]; // three rows of four ints each
for (int row = 0; row < 3; ++row) {
for (int col = 0; col < 4; ++col) {
shelf[row][col] = row * 10 + col;
}
}
for (int row = 0; row < 3; ++row) {
for (int col = 0; col < 4; ++col) {
std::cout << shelf[row][col] << ' ';
}
std::cout << '\n';
}
return 0;
}The output is the grid, filled row by row:
0 1 2 3
10 11 12 13
20 21 22 23How the memory is laid out
The array [[shelf[3][4]]] is not a grid of pointers. It is twelve consecutive int values, and the compiler works out the address of [[shelf[row][col]]] by multiplying out the offsets.
#include
int main() {
int shelf[3][4];
std::cout << "sizeof(shelf): " << sizeof(shelf) << '\n';
std::cout << "sizeof(shelf[0]): " << sizeof(shelf[0]) << '\n';
std::cout << "sizeof(shelf[0][0]): " << sizeof(shelf[0][0]) << '\n';
std::cout << "address of [0][0]: " << &shelf[0][0] << '\n';
std::cout << "address of [1][0]: " << &shelf[1][0] << '\n';
return 0;
}The addresses of [[[0][0]]] and [[[1][0]]] differ by exactly one row, which is the proof that the rows are laid out one after another:
sizeof(shelf): 48
sizeof(shelf[0]): 16
sizeof(shelf[0][0]): 4
address of [0][0]: 0x7ffef53cc720
address of [1][0]: 0x7ffef53cc730The two addresses are 16 bytes apart, which is exactly one row of four ints. The absolute values change on every run because the stack is placed at a random address, but the gap between them stays the same.
Because the rows are contiguous, the whole grid can also be walked as a single flat sequence of rows * cols elements.
Passing a grid to a function
A two-dimensional array decays to a pointer to its first row. The column count survives, because it is needed to compute the offset of an element; the row count does not, so it has to be passed separately.
#include
void print_grid(const int grid[][4], int rows) {
for (int row = 0; row < rows; ++row) {
for (int col = 0; col < 4; ++col) {
std::cout << grid[row][col] << ' ';
}
std::cout << '\n';
}
}
int main() {
int shelf[2][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}};
print_grid(shelf, 2);
return 0;
}The parameter [[const int grid[][4]]] is read as const int (*grid)[4] — a pointer to an array of four const ints. The 4 must be present, because without it the compiler cannot work out how to find [[grid[row][col]]].
The output is the grid printed row by row:
1 2 3 4
5 6 7 8The row pointer trap
One of the most common C++ bugs is confusing a pointer to a row with an array of rows. The types are different and the sizes differ.
#include
int main() {
int grid[2][3] = {{1, 2, 3}, {4, 5, 6}};
int (*row_pointer)[3] = grid; // pointer to the first row
std::cout << "sizeof(grid): " << sizeof(grid) << '\n';
std::cout << "sizeof(row_pointer): " << sizeof(row_pointer) << '\n';
std::cout << "grid[1][2]: " << grid[1][2] << '\n';
std::cout << "pointer[1][2]: " << row_pointer[1][2] << '\n';
return 0;
}The output shows that the grid is 24 bytes while the pointer is only 8:
sizeof(grid): 24
sizeof(row_pointer): 8
grid[1][2]: 6
pointer[1][2]: 6Both expressions reach the same element, because the pointer points at the first row and the arithmetic works out the same way. But the sizes are completely different, which is exactly why the types must be kept straight.
Three dimensions and beyond
The same rules extend. A three-dimensional array is an array of arrays of arrays, and it decays to a pointer to an array of arrays.
#include
int main() {
int cube[2][2][2];
int value = 1;
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 2; ++j) {
for (int k = 0; k < 2; ++k) {
cube[i][j][k] = value++;
}
}
}
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 2; ++j) {
for (int k = 0; k < 2; ++k) {
std::cout << cube[i][j][k] << ' ';
}
std::cout << '|';
}
std::cout << '\n';
}
return 0;
}The output shows the eight values in the order they were stored:
1 2 |3 4 |
5 6 |7 8 |Filling a grid and summarising it
Grids earn their keep when you are holding tabular data. Here is a small sales table, and the column totals computed from it.
#include
int main() {
const int rows = 3;
const int cols = 3;
int sales[rows][cols] = {
{12, 18, 9},
{22, 15, 30},
{8, 11, 27}
};
int column_totals[cols] = {0, 0, 0};
int grand_total = 0;
for (int r = 0; r < rows; ++r) {
for (int c = 0; c < cols; ++c) {
column_totals[c] += sales[r][c];
grand_total += sales[r][c];
}
}
for (int c = 0; c < cols; ++c) {
std::cout << "column " << c << " total: " << column_totals[c] << '\n';
}
std::cout << "grand total: " << grand_total << '\n';
return 0;
}The output shows the totals:
column 0 total: 42
column 1 total: 44
column 2 total: 66
grand total: 152When a vector of vectors is better
Raw multidimensional arrays force every dimension to be a compile-time constant, and they are awkward to pass around and to resize. std::vector of std::vector trades a little performance for flexibility.
#include
#include
int main() {
std::vector> shelf;
for (int row = 0; row < 3; ++row) {
std::vector books;
for (int col = 0; col < 4; ++col) {
books.push_back(row * 10 + col);
}
shelf.push_back(books);
}
for (const auto& row : shelf) {
for (int value : row) {
std::cout << value << ' ';
}
std::cout << '\n';
}
return 0;
}The output is identical to the raw array version, and the rows can now be different lengths and can grow at runtime.
0 1 2 3
10 11 12 13
20 21 22 23Quick reference
- A multidimensional array is a contiguous block; rows are laid out one after another.
sizeof(grid)is the whole size; [[sizeof(grid[0])]] is one row.- Passing a grid decays to a pointer to its first row; the column count must stay in the signature.
- A row pointer (
int (*p)[4]) is not the same type as an array of rows. - More than three dimensions is a sign you want a struct or a class instead.
- Use
std::vectorofstd::vectorwhen the dimensions vary or change at runtime.
Try it yourself: build a three-by-three grid of book ratings, print it, and print the sum of each row and of each column. Then write a function that takes the grid and returns the highest value, with the row and column count passed in, and call it with a grid of a different size to see why the row count has to be separate. Finally, convert the grid to a [[std::vector]] of [[std::vector]] and confirm the output is unchanged.