Multidimensional Arrays
A regular array has one dimension in the sense that it’s a single row of values. A two-dimensional array is a grid, with values arranged in rows and columns.
Declaring a two-dimensional array requires specifying both dimensions. Here’s how you would declare such an array to track 4 test scores for a class of size 5:
int scores[5][4];Each row belongs to a student, and each column holds one of their test scores, so scores[5][4] reads as “5 students, 4 scores each.” In general, the first number is the number of rows, and the second is the number of columns.
To get or set a specific score, you use two indices instead of one:
scores[0][0] = 92; // student 0, first testscores[0][1] = 88; // student 0, second testscores[2][3] = 75; // student 2, fourth testThe same indexing rule applies as for a one-dimensional array. It starts at 0, the last valid row index is 5 - 1, and the last valid column index is 4 - 1.
How the grid actually sits in memory
Under the hood, a 2D array isn’t organized as a grid. It’s stored as one continuous block, just like a regular array.
Picture scores[5][4] as 20 int slots laid out back to back. The first 4 slots are row 0, the next 4 are row 1, and so on. This layout is called row-major order, meaning entire rows are stored one after another.
scores[5][4] laid out in memory as 20 slots.
Take scores[2][3]. The compiler needs to turn row index 2 and column index 3 into a single slot number, so it can access the memory the same way it does for a 1D array. It does this by skipping past the first 2 full rows (2 rows × 4 columns each = 8 slots), then moving 3 more slots into the next row, landing on slot 8 + 3 = 11. As a general formula, that’s row_index * number_of_columns + column_index.
Why bother? You don’t need to do that math yourself, the compiler handles it. But knowing it’s happening explains a few things. For one, you now know why you must specify the number of columns up front, since the compiler needs it to calculate each element’s slot. For another, you know why reading the elements row by row tends to be faster than column by column, since that matches how the data is actually stored in memory.
Setting values when you declare the array
Just like a regular array, you can initialize a 2D array at the point of declaration, using nested curly braces, one pair per row:
int scores[3][2] = { {92, 88}, // student 0 {79, 85}, // student 1 {95, 91} // student 2};If you leave some values out, the rest default to zero. And if you initialize every row, but leave out the number of rows in the declaration, then the compiler infers it for you:
You can’t drop the number of columns, though. The compiler needs to know how wide each row is to calculate where each row begins.
Walking the grid with nested loops
To process every element, you need a nested loop. The outer loop walks the rows and the inner loop walks the columns within each row.
The inner loop finishes completely before the outer loop moves to the next student. That matches how the data is actually stored: you sweep across one row, then move to the next.
Going beyond two dimensions
Nothing stops you from adding a third dimension, or more. A common case is a stack of grids, like a 3D board game, or RGB color data for an image (rows, columns, and 3 color channels):
int pixels[480][640][3]; // 480 rows, 640 columns, 3 color channelsThe same row-major logic extends to higher dimensions. Accessing pixels[row][col][channel] means the compiler steps through all 3 channel values first, then moves to the next column, then after moving through all 640 columns, moves to the next row.
Passing an array to a function
Grids are a natural fit for board games. Here’s a minimal tic-tac-toe board, stored as characters, with a function to print it:
The call on line 21, printBoard(board);, looks no different from passing a one-dimensional array. The function parameter board[3][3] on line 3 indicates that a 2-dimensional array with 3 rows and 3 columns is expected.
Beyond printing: Building out full tic-tac-toe logic is beyond the scope of this lesson, but it’s good practice working with indices. Placing a move is just an assignment (
board[1][0] = 'O';puts anOin the middle-left cell). Checking for a winner would mean scanning rows, columns, and diagonals.
Which dimensions you can omit
When you pass an array to a function, you have to specify every dimension except the first one. So, this one compiles fine:
void printBoard(char board[][3]) { /* ... */ }But this one doesn’t:
void printBoard(char board[][]) { /* ... */ } // error: column size missingThat’s because accessing board[row][col] requires the compiler to know the number of columns in that array, so it can find the right slot (the same row * number_of_columns + col formula from earlier).
The row count, by contrast, isn’t needed for that calculation, so it can be omitted. It’s common to pass it as a separate argument anyway, so the function knows how many rows to loop through:
void printGrid(int grid[][4], int numRows) { /* ... */ }Checkpoint
Given this declaration, what slot number does grid[2][3] map to in memory?
int grid[4][6];Which of these declarations compiles?
A function needs to accept a grid of monthly rainfall totals, where the number of columns (months) is always 12, but the number of rows (cities) varies between calls. Which signature works?