In a declaration T D where D has the form

D1 [constant-expression_{opt}]attribute-specifier-seq_{opt}

and the type of the identifier in the declaration
T
D1
is
“derived-declarator-type-list
T”,
then the type of the identifier of
D
is an array type; if the type of the identifier of D
contains the auto * type-specifier*,
the program is ill-formed.
T
is called the array
*element type*;
this type shall not be a reference type, the (possibly cv-qualified) type
void,
a function type or an abstract class type.
If the
*constant-expression*
([expr.const]) is present, it shall be a converted constant
expression of type std::size_t and
its value shall be greater than zero.
The constant expression specifies the
*bound*
of (number of elements in) the array.
If the value of the constant expression is
N,
the array has
N
elements numbered
0
to
N-1,
and the type of the identifier of
D
is “* derived-declarator-type-list* array of
N
T”.
An object of array type contains a contiguously allocated non-empty set of
N
subobjects of type
T.
Except as noted below, if
the constant expression is omitted, the type of the identifier of
D
is “* derived-declarator-type-list* array of unknown bound of
T”,
an incomplete object type.
The type “* derived-declarator-type-list* array of
N
T”
is a different type from the type
“* derived-declarator-type-list* array of unknown bound of
T”,
see [basic.types].
Any type of the form
“* cv-qualifier-seq* array of
N
T”
is adjusted to
“array of
N
* cv-qualifier-seq*
T”,
and similarly for
“array of unknown bound of
T”.
The optional *attribute-specifier-seq* appertains to the array.
[ *Example:*

typedef int A[5], AA[2][3]; typedef const A CA; // type is “array of 5 const int” typedef const AA CAA; // type is “array of 2 array of 3 const int”

* — end example* ]
[ *Note:*
An
“array of
N
* cv-qualifier-seq*
T”
has cv-qualified type; see [basic.type.qualifier].
* — end note* ]

An array can be constructed from one of the fundamental types (except void), from a pointer, from a pointer to member, from a class, from an enumeration type, or from another array.

When several “array of” specifications are adjacent, a multidimensional
array is created;
only the first of
the constant expressions that specify the bounds
of the arrays may be omitted.
In addition to declarations in which an incomplete object type is allowed,
an array bound may be omitted in some cases in the declaration of a function
parameter ([dcl.fct]).
An array bound may
also be omitted
when the declarator is followed by an
*initializer*
([dcl.init]).
In this case the bound is calculated from the number
of initial elements (say,
N)
supplied
([dcl.init.aggr]), and the type of the identifier of
D
is “array of
N
T.”
Furthermore, if there is a preceding declaration of the entity in the same
scope in which the bound was specified, an omitted array bound is taken to
be the same as in that earlier declaration, and similarly for the definition
of a static data member of a class.

float fa[17], *afp[17];

declares an array of float numbers and an array of pointers to float numbers. For another example,

static int x3d[3][5][7];

declares a static three-dimensional array of integers, with rank 3 × 5 × 7. In complete detail, x3d is an array of three items; each item is an array of five arrays; each of the latter arrays is an array of seven integers. Any of the expressions x3d, x3d[i], x3d[i][j], x3d[i][j][k] can reasonably appear in an expression. Finally,

extern int x[10]; struct S { static int y[10]; }; int x[]; // OK: bound is 10 int S::y[]; // OK: bound is 10 void f() { extern int x[]; int i = sizeof(x); // error: incomplete object type }

* — end example* ]

[ *Note:*
conversions affecting expressions of array type are described in [conv.array].
Objects of array types cannot be modified, see [basic.lval].
* — end note* ]

[ *Note:*
Except where it has been declared for a class ([over.sub]),
the subscript operator
[]
is interpreted
in such a way that
E1[E2]
is identical to
*((E1)+(E2)).
Because of the conversion rules
that apply to
+,
if
E1
is an array and
E2
an integer,
then
E1[E2]
refers to the
E2-th
member of
E1.
Therefore,
despite its asymmetric
appearance, subscripting is a commutative operation.

A consistent rule is followed for
multidimensional arrays.
If
E
is an
*n*-dimensional
array
of rank
i × j × … × k,
then
E
appearing in an expression
that is subject to the array-to-pointer conversion ([conv.array])
is converted to
a pointer to an (n-1)-dimensional
array with rank
j × … × k.
If the
*
operator, either explicitly
or implicitly as a result of subscripting,
is applied to this pointer,
the result is the pointed-to (n-1)-dimensional array,
which itself is immediately converted into a pointer.

[ *Example:*
consider

int x[3][5];

Here
x
is a 3 × 5 array of integers.
When
x
appears in an expression, it is converted
to a pointer to (the first of three) five-membered arrays of integers.
In the expression
x[i]
which is equivalent to
*(x+i),
x
is first converted to a pointer as described;
then
x+i
is converted to the type of
x,
which involves multiplying
i
by the
length of the object to which the pointer points,
namely five integer objects.
The results are added and indirection applied to
yield an array (of five integers), which in turn is converted to
a pointer to the first of the integers.
If there is another subscript the same argument applies
again; this time the result is an integer.
* — end example* ]
* — end note* ]