配列型（はいれつがた）は、特定の型のオブジェクトの集合を連続して割り付けたものです『JISX3010:2003』p.24「6.2.5 型」。
配列型を使うことで、同じ型の複数のデータを、共通の名前で、添字（そえじ）と呼ばれる番号を用いて、アクセスすることができます『JISX3010:2003』p.51「6.5.2.1 配列の添字付け」。
例えば、10個の変数の合計を求めるプログラムは、配列を用いて次のように書き換えることができます。
[https://paiza.io/projects/g54dP_C98WhNm8KfKyiAfw?language=c スカラー変数を使ったコード]  ※ここでの「スカラー」とは、配列ではない1個だけのデータを入れるデータ型であるという意味 : 
int main(void) {
  const int i0 = 2, i1 = 3, i2 = 5, i3 = 7, i4 = 11,
      i5 = 13, i6 = 17, i7 = 19, i8 = 23, i9 = 29;
  int sum = i0 + i1 + i2 + i3 + i4 + i5 + i6 + i7 + i8 + i9;
  printf("%d\n", sum);
}
[https://paiza.io/projects/2S6AqxK8v_4GJnaXv2sUYw?language=c 配列変数を使ったコード] : 
int main(void) {
  const int ary[] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
  int sum = 0;
  for (int i = 0; i < sizeof ary / sizeof *ary; i++) {
    sum += ary[i];
  }
  printf("%d\n", sum);
}
配列を宣言すると同時に要素を初期化リストで初期化すると、要素数は省略できます。
また、ループカウンターやインデックスの変数名には慣例的に i が使われます（入れ子の場合は j, k ... とすることが多いです。また i1, i2 ... という流儀もありますが、一貫していることが大切です）。
[https://paiza.io/projects/eheFNAJeQbKuYZjxVp0o2w?language=c 複合リテラルを使ったコード] : 
int main(void) {
  int sum = 0;
  for (int i = 0, x = 0; x=(int[]){ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, -1}[i], x >= 0; i++)
    sum += x;
  printf("%d\n", sum);
}
複合リテラルの長さを得ることは難しいので、負の要素を終端子としました。文字列の終端子 '\0' の要領です。
1次元の配列の宣言の記述は次のようになります。
配列変数の宣言の構文:
データ型 配列変数名 [ 配列の要素数 ];
データ型:組込み型あるいはユーザー定義型
配列変数名:キーワードを除いた有効な識別子
配列の要素数:0より大きな整数。定数式の他、一定の条件を満たせば実行時に確定する値。
配列変数の宣言:
int main(void) {
	int ary[3]; // int型で要素数3の配列変数 ary の宣言
}
上の例では、int型でaryという名前で3つの要素を持つ配列を宣言しています。
3つの要素は配列名のあとに[]に囲まれた添字と呼ばれる番号を付けて区別します。
添字は[0]から順番に数え、[配列の要素数-1] までの整数です。
つまり ary[0]、ary[1]、ary[2] というようになります。
また、配列は、宣言と同時に、値のリストで初期化することもできます。
初期化していないローカルな配列の値は不定です。
初期化の記述は次のようになります『JISX3010:2003』p.94「6.7.8 初期化」。
初期化リストによる配列変数の初期化:
データ型 配列名[配列の要素数] = { 値のリスト };
値のリストは、コンマで区切った定数のリストです。
値のリストの定数で、対応する配列の要素の値を初期化します。
値のリストが配列の要素数より多い場合、警告を発するかもしれませんが、コンパイルエラーとなりません。
値のリストが配列の要素数より少ない場合、残りの要素は0で初期化されます。
配列変数の宣言と初期化:
int main(void) {
  int ary[3] = { 1, 2, 3 };
}
ary[0]が1、ary[1]が2、ary[2]が3で初期化されます。
初期化される場合は配列の要素数は省略可能:
int main(void) {
  int ary[] = { 1, 6, 8 };
}
値のリストの要素数で配列の要素数が決まります。
1次元配列の要素への代入の記述は次のようになります。
配列名[添字] = 式;
配列名が指す配列の「添字」で指定した順位の要素に式の値を代入します。
添字は、アクセスする配列の要素を指定するもので整数型であれば変数でも構いません。
添字は 0 から始まり、配列の要素数から 1 を減じた値を取りえる整数値です。
配列の要素に代入:
int main(void) {
	int ary[4];
	ary[0] = 2;
    ary[1] = 4;
    ary[2] = 8;
    ary[2] = 16;
}
指示子(''designator'')による配列変数の初期化:
データ型 配列名[配列の要素数] = { 指示子のリスト };
指示子リストは、コンマで区切った指示子のリストです。
配列の指示子は[ 要素番号 ] = 定数式の形式で、要素番号を指定して定数式で初期値を指定します。
指示子で初期化されなかった要素は0で初期化されます。
配列の要素数が指定されない場合、指示子で指定された最大の要素番号＋１が配列の要素数になります。
[https://paiza.io/projects/H4925tu8cd6cl_sD2ohZhQ?language=c 指示子による配列変数の初期化] : 
int main(void) {
  int i[] = {
      [2] = 1,
      [3] = 1, 
      [5] = 1, 
      [7] = 1,
      [11] = 1,
  };
  for (int n = 0; n < sizeof i / sizeof *i; n++)
    printf("%d: %d, ", n, i[n]);
}
実行結果:
0: 0, 1: 0, 2: 1, 3: 1, 4: 0, 5: 1, 6: 0, 7: 1, 8: 0, 9: 0, 10: 0, 11: 1, 
1次元配列の参照の構文は次のようになります。
構文:
配列 [ 添字 ]
「配列」指す配列の[添字]番目の要素を参照します（代入も左辺値参照ですので、参照の一種です）。
「配列」は大概は配列変数ですが、複合リテラル（例：(int[]){2,3,5,7}）など名前を持たない値であっても構いません。
配列要素の参照:
int main(void) {
	int ary[] = { 0, 10, 20 }; // 要素数3のint型配列を宣言し 0, 10, 20 で初期化
	ary[0] = ary[1] * ary[2];  // 配列の[1]と[2]の数値を参照し、配列の[0]にその和を代入。
}
ary[1]とary[2]の要素が参照され、その積をary[0]の要素に代入しています。
注意！
C言語では、配列要素の参照に際し、添字の値が配列の範囲内に入っているかのチェックはコンパイル時にもランタイムにも行われません。
プログラマーは、配列参照の添字の範囲が配列の範囲内であることの全責任を負います。
配列参照の添字には定数式以外にも変数や関数の戻り値を使うこともできるので添字安全性を担保することには、ときに困難が伴います。
多次元配列の宣言は、配列名の後に、次元の数だけ[配列の要素数]を追加します。
例えば3行×4列の2次元配列の宣言は、次のように記述できます。
多次元配列の宣言:
int main(void) {
  int ary[3][4];
}
上の例では、次の表のように3行×4列の配列が確保されます。
実際には、次の表のような順番で配列がメモリ上に確保されます。
[https://paiza.io/projects/XXw8x1xcAm6RJnLDV8amOw?language=c 二次元配列の要素のアドレス]:
int main(void) {
  int ary[3][4];
  const int rows = sizeof ary / sizeof *ary, cols = sizeof *ary / sizeof **ary;
  printf("rows = %d, cols = %d; sizeof(int) = %zu\n", rows, cols, sizeof(int));
  for (int i = 0; i < rows; i++)
    for (int j = 0; j < cols; j++)
      printf("Address of ary[%d][%d] = %p\n", i, j, &ary[i][j]);
}
実行結果 :
rows = 3, cols = 4; sizeof(int) = 4
Address of ary[0][0] = 0x7ffccf262e50
Address of ary[0][1] = 0x7ffccf262e54
Address of ary[0][2] = 0x7ffccf262e58
Address of ary[0][3] = 0x7ffccf262e5c
Address of ary[1][0] = 0x7ffccf262e60
Address of ary[1][1] = 0x7ffccf262e64
Address of ary[1][2] = 0x7ffccf262e68
Address of ary[1][3] = 0x7ffccf262e6c
Address of ary[2][0] = 0x7ffccf262e70
Address of ary[2][1] = 0x7ffccf262e74
Address of ary[2][2] = 0x7ffccf262e78
Address of ary[2][3] = 0x7ffccf262e7c
16進数ですが要素ごとのアドレスが4(=sizeof(int))づつ離れているのが判ります（アドレスと、その増分は環境や処理系によって変わります）。
多次元配列も1次元配列と同様に、配列の宣言と同時に値のリストで初期化することができます。
値のリストは、各次元の添字が[0]のものから始まり、一番右の次元の添字が1つずつ増えていき、その添字の数が要素数まで増えたら、1つ左の次元へ繰り上がる。
多次元配列の宣言と初期化:
int main(void)
{
	int ary[3][4] = {
		{ 0, 1, 2, 3 },
		{ 4, 5, 6, 7 },
		{ 8, 9, 10, 11 }
	};
}
上の例では、次の表のように初期化されます。
多次元配列を値のリストで初期化する場合、最上位次元の要素数は省略できます。
最上位次元の要素数を省略して初期化:
int main(void) {
	int ary[][4] = {
		{ 0, 1, 2, 3 },
		{ 4, 5, 6, 7 },
		{ 8, 9, 10, 11 }
	};
}
多次元配列でも、指示子 を使った初期化ができます。
指示子を使った初期化:
int main(void) {
	int ary[][4] = {
		[1] = { 4, 5, 6, 7 },
		[0] = { 0, 1, 2, 3 },
		[2] = { 8, 9, 10, 11 }
	};
}
多次元配列の代入は、配列名の後に、次元の数だけ[添字]を追加します。
多次元の配列の要素への代入:
int main(void) {
  int ary[3][4];
  const int rows = sizeof ary / sizeof *ary, cols = sizeof *ary / sizeof **ary;
  int n = 0;
  for (int y = 0; y < rows; y++)
    for (int x = 0; x < cols; x++)
      ary[y][x] = n++;
}
上の例では、次の表のように、代入されます。
多次元配列の参照も同様に、配列名の後に次元の数だけ[添字]を追加します。
//例 多次元の配列を参照します。
int main(void)
{
	int ary[3][4] = {
		0,1,2,3,
		4,5,6,7,
		8,9,10,11
	};
	int x, y;
	for (int y = 0; y < 3; y++){
		for (int x = 0; x < 4; x++){
			printf("%2d ", ary[y][x]);
		}
		printf("\n");
	}
}
配列全体をコピーする場合、各要素を1つずつコピーしなければなりません配列全体のコピー（＝配列の代入）ができなこととを以って、配列はC言語の第一級オブジェクトでないと言われます。文字列も文字の配列（に番兵として '\0' で終端したもの）なので、配列と同様に文字列も第一級オブジェクトではありません。。
[https://paiza.io/projects/YZ9GzwwPchcfLVGOqN-OQA?language=c 配列の要素を1つずつコピー] : 
int main(void) {
  int ary1[] = {0, 1, 2};
  const int size = sizeof ary1 / sizeof *ary1;
  int ary2[size];
  // ary1の要素をary2の要素に１つ１つコピーする。
  for (int i = 0; i < size; i++)
    ary2[i] = ary1[i];
  for (int i = 0; i < size; i++)
    printf("ary2[%i] = %d\n", i, ary2[i]);
}
実行結果:
ary2[0] = 0
ary2[1] = 1 
ary2[2] = 2
memcpy関数を使用しても配列全体をコピーできます。
memcpy関数は、ヘッダー  で宣言されています。
[https://paiza.io/projects/Uzdan89U0tKjbeu09oNPbQ?language=c memcpy関数を使用しても配列全体をコピー] : 
int main(void) {
  int ary1[] = {0, 1, 2};
  const int size = sizeof ary1 / sizeof *ary1;
  int ary2[size];
  // memcpyを使用してary1をary2にコピーする。
  memcpy(ary2, ary1, sizeof ary1);
  for (int i = 0; i < size; i++)
    printf("ary2[%i] = %d\n", i, ary2[i]);
}
実行結果:
ary2[0] = 0
ary2[1] = 1 
ary2[2] = 2
{{コラム|memcpyとmemmove|
C言語の標準ライブラリーには、memcpyとmemmoveのよく似た2つの関数があります。
memcpyとmemmoveの違いは、ポインターが指すメモリ領域が重複している場合の動作です。
memcpyでは、メモリー領域が重複している場合は動作が保証されませんが、memmoveでは、メモリ領域が重複していても動作が保証されます。
そのため、重複がない場合は、どちらの場合も結果は同じになります。
今回の例では、重複していないことが自明なので memcpy を使いましたが、重複していないことに自信がない場合は memmove の使用を検討してください。
コピーしたい配列と同じシグネチャの要素を1つだけ持つダミーの構造体を用意して、コピー元・コピー先の配列のアドレスを、ダミー構造体にキャストした後に参照し「構造体のコピー」をさせることができます。
この方法ならば、
[https://paiza.io/projects/kPUVJyEPULcHI5VMKnQTIw?language=c 構造体を使った配列全体のコピー] : 
int main(void) {
  int ary1[] = {0, 1, 2};
  const int size = sizeof ary1 / sizeof *ary1;
  struct DUMMY { int _[size]; };
  int ary2[size];
  //  構造体を使った配列全体のコピー。
  *(struct DUMMY*)&ary2 = *(struct DUMMY*)&ary1
  for (int i = 0; i < size; i++)
    printf("ary2[%i] = %d\n", i, ary2[i]);
}
実行結果:
ary2[0] = 0
ary2[1] = 1 
ary2[2] = 2
コード生成の例:
*(struct DUMMY *)&ary2 = *(struct DUMMY *)&ary1
      33: 48 8b 45 f0                   movq    -16(%rbp), %rax
      37: 48 89 45 d0                   movq    %rax, -48(%rbp)
      3b: 8b 45 f8                      movl    -8(%rbp), %eax
      3e: 89 45 d8                      movl    %eax, -40(%rbp)
FreeBSD-12/amd64の標準cc（FreeBSD clang version 12.0.1 (git@github.com:llvm/llvm-project.git llvmorg-12.0.1-0-gfed41342a82f)）でコンパイルしました。
int(4バイト)x3要素のコピーですが、8バイトのコピーと4バイトのコピーを生成しています。
最適化をかけた結果を見ようとしましたが、最適化がかかるとコピーのコードそのものが蒸発してしまいました。
C99から、可変長配列がサポートされました。
[https://paiza.io/projects/yuxElF0Ep36P15M1hNVN_w?language=c 可変長配列の例]
int i = 2, j = 3, k, koo;
int *p = &i
int func(int n) {
  char b[ *p == n ? k = j++ : n + j];
  return (sizeof(b));
}
int main(void) {
  koo = func(10);
  printf("koo = %d\n", koo);
}
が一例です[http://www.open-std.org/jtc1/sc22/wg14/www/docs/n317.pdf ''Array of Variable length'']。
全ての配列を可変長にできるわけではなく要件がありまます。
バリアブルモディファイド（VM）型の宣言はすべて、ブロックスコープか、関数プロトタイプスコープのいずれかでなければなりません。
記憶クラス指定子が _Thread_local, static, extern のいずれかで宣言された配列オブジェクトは、可変長配列（VLA）型を持つことができません。
ただし、static 記憶クラス指定子で宣言されたオブジェクトは、VM 型（つまり VLA 型へのポインタ）を持つことができます。
最後に、VM型で宣言されたすべての識別子は通常の識別子でなければならず、構造体やユニオンのメンバーにはなれません。