Edit: Here's my attempt at a more to-the-point answer as requested and based on your new example code:
Regardless of the array dimensions, what you pass is a "pointer to an array" - it's only a single pointer, though the type of the pointer can vary.
In your first example, int array[6]
is an array of 6 int
elements. Passing array
passes a pointer to the first element, which is an int
, hence the parameter type is int *
, which can be equivalently written as int []
.
In your second example, int array[3][3]
is an array of 3 rows (elements), each containing 3 int
s. Passing array
passes a pointer to the first element, which is an array of 3 int
s. Hence the type is int (*)[3]
- a pointer to an array of 3 elements, which can be equivalently written as int [][3]
.
I hope you see the difference now. When you pass an int **
, it is actually a pointer to an array of int *
s and NOT a pointer to a 2D array.
An example for an actual int **
would be something like this:
int a[3] = { 1, 2, 3 };
int b[3] = { 4, 5, 6 };
int c[3] = { 7, 8, 9 };
int *array[3] = { a, b, c };
Here array
is an array of 3 int *
s, and passing this as an argument would result in an int **
.
Original answer:
Your first example isn't really a 2D array, although it is used in a similar way. There, you're creating ROWS
number of char *
pointers, each of which points to a different array of COLS
characters. There are two levels of indirection here.
The second and third examples are actually 2D arrays, where the memory for the entire ROWS * COLS
characters is contiguous. There is only one level of indirection here. A pointer to a 2D array is not char **
, but char (*)[COLS]
, so you can do:
char (*p)[SIZE] = arr;
// use p like arr, eg. p[1][2]