A multidimensional array is an array whose elements are themselves arrays. int grid[3][4]; is not a special grid type - it is three elements, each of which is an array of four ints, stored back to back. Once that clicks, the rest of the topic follows: the layout, the indexing arithmetic, and the otherwise-baffling rule about passing them to functions.
Two dimensions covers almost every practical use - grids, tables, matrices, game boards, images - so that is what this page works with.
Declaring and Initializing
int grid[3][4]; // 3 rows, 4 columns - 12 ints
The first number is how many rows, the second how many columns. Initializers can be written flat or with inner braces; the braces are worth using because they show the shape.
The int e[][3] form matters: you may leave the row count blank and let the initializer decide, but the column count is never optional. The next section explains why.
Row-Major Layout
C stores a 2D array in row-major order: the whole of row 0, then the whole of row 1, and so on, in one unbroken block of memory. There is no array of row-pointers behind the scenes.
int grid[3][4] = {{ 1, 2, 3, 4},
{ 5, 6, 7, 8},
{ 9,10,11,12}};
how you picture it how it actually sits in memory
+----+----+----+----+
| 1 | 2 | 3 | 4 | row 0 +--+--+--+--+--+--+--+--+--+--+--+--+
+----+----+----+----+ | 1| 2| 3| 4| 5| 6| 7| 8| 9|10|11|12|
| 5 | 6 | 7 | 8 | row 1 +--+--+--+--+--+--+--+--+--+--+--+--+
+----+----+----+----+ \__ row 0 __/\__ row 1 __/\_ row 2 _/
| 9 | 10 | 11 | 12 | row 2
+----+----+----+----+ grid[i][j] is at element index i*4 + j
That formula, i * columns + j, is the entire mechanism - and it is why the compiler must know the column count to index anything. The row count never enters the calculation.
You can see the layout directly by printing addresses:
The addresses climb by sizeof(int) with no gaps, including where one row ends and the next begins. The flattened loop proves it - flat[k] walks all twelve elements as a single run.
This layout also has a performance consequence worth knowing: looping rows-then-columns touches memory in order, which the CPU cache likes. Swapping the loop nesting so the inner loop steps down a column instead jumps by a whole row each time and can run several times slower on a big array.
Nested Loops
Two dimensions want two for loops: the outer picks the row, the inner sweeps that row's columns.
Name the counters after what they mean (i/row for rows, j/col for columns) and keep the order consistent - grid[row][col] everywhere. Half of all 2D-array bugs are a transposed pair of indices.
The #defined sizes are not decoration either: the loop bounds and the declaration now cannot drift apart when you change the shape.
Passing a 2D Array to a Function
Here is the rule that trips everyone: the function's parameter must declare the column count.
The reason is decay. Passing grid converts it to a pointer to its first element - and its elements are rows, so the type is int (*)[4]: pointer to an array of 4 ints. For grid[i][j] to mean anything, the compiler must know how far one row is, and that is the 4. The row count is genuinely absent from the type, which is why it travels as a separate argument.
Note int (*grid)[COLS] and int grid[][COLS] are the same parameter written two ways - the parentheses are required, since int *grid[COLS] would be an array of pointers instead. That distinction is covered in pointers and arrays.
If the column count is only known at runtime, C99's variably-modified parameters let you pass it first:
void print_any(int rows, int cols, int grid[rows][cols]);
rows and cols must be declared before the array parameter that uses them. Where that is unavailable, the common alternative is a flat 1D array plus manual index arithmetic:
data[i * cols + j] is exactly what the compiler writes for you in the fixed-size case. Doing it by hand costs one line and works for any shape decided at runtime.
A Matrix Example
Matrix multiplication puts the whole page together - three nested loops over row-major storage.
Two details worth copying. The inner k loop pairs a[i][k] with b[k][j] - one index walks a row, the other walks a column. And the transpose starts its inner loop at j = i + 1: starting at 0 would swap every pair twice and leave the matrix unchanged.
Three Dimensions and Beyond
The pattern extends, and so does the rule about function parameters - every dimension except the first must be declared.
In practice, three dimensions is where fixed-size arrays start to feel unwieldy, and most code switches to a flat block with computed indices or an array of structs that names what each axis means.
Common Mistakes
- Writing
grid[i, j]. The comma operator evaluatesi, discards it, and indexes withj. It compiles. It is wrong. Usegrid[i][j]. - Transposing the indices.
grid[col][row]reads a real element from the wrong place, so there is no error to catch it. Keep the[row][col]order everywhere. - Omitting the column size in a parameter.
void f(int grid[][])does not compile, and that is the compiler saving you. - Going out of bounds. As with any array, there is no bounds check.
grid[0][5]on a[3][4]grid silently readsgrid[1][1], because the layout is contiguous and the arithmetic does not care.
Frequently Asked Questions
How do you declare a 2D array in C?
Give two sizes in brackets: int grid[3][4]; declares 3 rows of 4 columns - 12 ints total. Read it as "an array of 3 things, each of which is an array of 4 ints", which is literally how C stores it.
How is a 2D array stored in memory in C?
In row-major order: all of row 0's elements, then all of row 1's, and so on, in one contiguous block. grid[i][j] lives at offset i * columns + j elements from the start, which is why the column count is the number the compiler needs.
How do you pass a 2D array to a function in C?
The parameter must declare the column count: void print(int grid[][4], int rows) or equivalently void print(int (*grid)[4], int rows). The row count may be omitted because the array decays to a pointer to a row - but without the column size the compiler cannot compute where a row begins.
Can you initialize a 2D array to all zeros?
Yes: int grid[3][4] = {0}; zeroes every element, because any element you do not list is zero-initialized. int grid[3][4] = {{1, 2}}; sets the first two entries of row 0 and leaves the other ten at zero.