As with 1D arrays, a declaration such as
int a[MAX_ROWS][MAX_COLS];
allocates a collection of integer cells, and a pointer which is given the name, a, which is initialized to point to the beginning of the memory allocated. However, a 2D array must still be stored in linear memory. The convention for C is that the 2 dimensions of the array are stored in row major order in memory. This means that all of the elements of row 0 are stored, followed by all the elements of row 1, then row 2, etc.
This essentially means that the 2D array is stored in one big row. The first row is in memory first. Right next to it in the same row is the second row of memory. This continues for all rows of the 2D array.
As with 1D arrays, the name of the array is a pointer to the beginning of the memory block allocated. In order to calculate the address of an arbitrary cell in the array, a[i][j], the compiler uses an expression like:
*(a + i * MAX_COLS * sizeof(int) + j * sizeof(int))
To evaluate such an expression inside a function, the function must know the number of columns in the structure.
So, to pass the array, we must declare it in the parameter list like:
process(int a[][MAX_COLS])
NOTE: We MUST provide the number of columns in the declaration.
As we can see, 2D arrays are implemented and accessed with pointers, just like 1D arrays.
a <===> &a[0][0]
a[0] <===> &a[0][0]
a[1] <===> &a[1][0]
...
a[i] <===> &a[i][0]
Essentially, a single subscripted expression for a 2D array refers to a 1D array - i.e. one row.
We rarely use the pointer expressions for individual cells in a 2D array.
C also allows multi-dimension arrays:
float b[MAX_ROWS][MAX_COLS][MAX_DEPTH];
for a 3D array.