You are asking good questions about C. I'm happy, in fact, to see them. Most folks just "move on" and get on with things that seem to work and never do "dig in" when their mental models are at question. You SHOULD ask. Others SHOULD ask. First off, take a look at the link below. Part of what you are asking is handled there. Part is handled by "thinking like a compiler."
When you write:
#define ROWS 5
#define COLS 5
int my_array[ROWS][COLS];
What happens?
The C compiler generates instructions to the linker to allocate a consecutive region able to hold 25 integer values and tells the linker to give that region a symbolic name of "my_array." my_array is then just a linker variable and once the linker figures out the address, everywhere the linker sees 'my_array' in the compiler output it stuffs that address value there, instead.
Before I go further, what does the C compiler do when you write:
my_array[3][2]
So the C compiler sees 'my_array' as a "pointer to an array of COLS integers." Or "int (*)[COLS]" in C-speak. When you pass this as a parameter to printf(), you won't get any errors since printf() parameters can have any type at all. What C will do is write out 'my_array' in the object file so that once the linker figures out where the array is at it can stuff that address there. You write 'my_array' and C emits "when you get an address for my_array, put the value here" to the linker. So the linker does.
The C compiler sees '&my_array' as a "pointer to an array of ROWS arrays of COLS integers." Or "int (*)[ROWS][COLS]" in C-speak. Different type. But that's okay. When you put that in a parameter to printf(), the C compiler emits "when you get an address for my_array, put the value here" to the linker. So the linker does. And you get the same value, too. Why? Because the linker doesn't know any better. The C compiler told it to put down whatever my_array's value is. And the C compiler doesn't know any better, either, because prinft() accepts anything. So the C compiler can't even flag an error.
The compiler sees '*my_array' as a "pointer to an integer." Or "int (*)" in C-speak. Remember, 'my_array' is "int (*)[COLS]" in C-speak. Dereferencing that means "int (*)". Do you see a pattern now?
my_array int (*)[COLS]
&my_array int (*)[ROWS][COLS]
*my_array int (*)
But every one of them still a pointer. So we could now guess that:
**my_array int
And you'd be right.
Array syntax is really just a convenience for pointer syntax. I think that is the bottom line. Arrays are kind of "pasted on" afterwards and not designed in at the beginning so much. Pointers are real meat and potatoes, arrays are just sugar.
Say I had this:
int a[10];
When I write a[3] the C compiler immediately turns that into (*((3)+(a))) without missing a heartbeat. It never even sees the brackets. It's all over before it starts.
So when you write my_array[3][2], the C compiler immediately asks itself "What type is my_array?" And gets "int (*)[COLS]" as the answer. The following [3] then tells the C compiler to use the 4th array of COLS integers, so the C compiler generates:
1. (*(((3)*COLS)+(my_array)))
But also notices that because of the dereference that the type is now "int (*)". Still a pointer. So now it encounters the [2] and says:
2. (*((2)+(*(((3)*COLS)+(my_array)))))
And now that is an integer! No longer a pointer.
So what happens when the compiler sees my_array[0]? Well, just what I wrote in (1) above, except that the 3 is replaced with 0:
3. (*(((0)*COLS)+(my_array)))
What's the type? Just as I wrote before: "int (*)". So it is still a pointer to memory. Yup. The C compiler sticks 'my_array' there for the linker to stuff with the address, once again. So you get to see the same thing printed out, again. Because the type was still a pointer. A different type of pointer, true. But a pointer, still.
So all were still just pointers.
By the way, just to really make the point that arrays are just sugar and pointers are the real deal, look at this. If I defined:
4. int a[10];
Then,
5. 5[a]
is perfectly legal and says the same thing as a[5].
Think about it and why.
EDIT: I said that &my_array is "int (*)[ROWS][COLS]". How might you use that thing? Well, you could write,
6. int j= (&my_array)[2][3][4];
It means you want the 3rd matrix, the 4th row, and the 5th column.
It's a pointer to an array of 2D integer matrices. my_array is a pointer to an array of integer vectors (1D). and *my_array is a pointer to a single vector of integers. And finally **my_array is an integer.