Given your declarations, your array looks like this in memory (assuming a 4-byte int):
addr Expression type
offset
int int [3] int [2][3]
---- --- ------- ----------
+====+
0x00 | 20 | A[0][0] A[0] A
+––––+
0x04 | 30 | A[0][1]
+––––+
0x08 | 40 | A[1][2]
+====+
0x0c | 50 | A[1][0] A[1]
+––—–+
0x10 | 60 | A[1][1]
+––––+
0x14 | 70 | A[1][2]
+====+
There's no metadata for array size, or for the location of the first element, or anything like that stored as part of the array - it's just a sequence of objects.
The key thing to notice is that the address of an array is the same as the address of its first element - the address of A is the same as the address of A[0], which is the same as the address of A[0][0]. The expressions &A, &A[0], and &A[0][0] will all yield the same address value (although the types of each expression will be different, which may affect value representation)1.
Unless it is the operand of the unary & operator, an expression of type "N-element array of T" is converted, or "decays", to an expression of type "pointer to T" and the value of the expression is the address of the first element in the array.
The expression A has type "2-element array of 3-element array of int" (int [2][3]). Unless it is the operand of the unary & operator, it "decays" to type "pointer to 3-element array of int" (int (*)[3]) and its value is the address of A[0]. The expression A[0] has type "3-element array of int" (int [3]). Unless it is the operand of the unary & operator, it "decays" to type "pointer to int" (int *) and its value is the address of A[0][0].
Why is that? Array indexing in C is based on pointer arithmetic. The expression a[i] is defined as *(a + i) - given an address value a, offset i elements (not bytes!) from that address and dereference the result. This is taken from the B programming language from which C is derived. The problem is that B set aside an explicit pointer to the first element of the array - the declaration
auto A[N];
created the following in memory:
+---+
A: | | ---+
+---+ |
... |
+------+
|
v
+---+
| | A[0]
+---+
| | A[1]
+---+
...
Ritchie wanted to keep B's array behavior - a[i] == *(a + i) - but he didn't want to keep the explicit pointer that behavior required. So he created the rule that anytime the compiler sees an array expression that's not the operand of the unary & operator, it will convert the expression to a pointer.
If A "decays" to an expression of type int (*)[3] and is the address of A[0], then it follows that the expression *A has type int [3] (which in turn "decays" to int *) and is the value of A[0]:
A == &A[0] // int (*)[3] == int (*)[3]
*A == A[0] // int [3] == int [3] ==> int * == int *
Here it is in handy table form:
Expression Type Decays to Equivalent Expression
–––––––––– –––– ––––––––– –––––––––––––––––––––
A int [2][3] int (*)[3] &A[0]
&A int (*)[2][3] n/a n/a
*A int [3] int * A[0]
A[i] int [3] int * &A[i][0]
&A[i] int (*)[3] n/a n/a
*A[i] int n/a A[i][0]
- Pointers to different types don't have to all have the same size or representation - that depends on the implementation. For modern server and desktop architectures like x86/x86-64, all object pointer types have the same size as the native word size (32 or 64 bits). However, the language definition only requires the following:
char * and void * have the same size and alignment;
- Pointers to qualified types have the same size and alignment as pointers to their unqualified equivalents (e.g.,
sizeof (const int) == sizeof (volatile int) == sizeof (int))
- All
struct pointer types have the same size and alignment;
- All
union pointer types have the same size and alignment;