配列とは、同じ型の要素を固定個数だけメモリ上に連続して並べたブロックです。この連続配置こそがすべての出発点です。インデックス指定が一瞬で済むのも、配列とポインタが密接に結びついているのも、C言語が先頭要素のアドレスだけを渡して残りを算術で辿らせられるのも、すべてここから来ています。
配列を宣言する
要素の型、名前、そして角括弧に入れたサイズです:
int scores[5]; // int が 5 個
double prices[100]; // double が 100 個
char initials[3]; // char が 3 個
サイズは要素の個数であり、配列を宣言する場所で分かっていなければなりません。通常はリテラルか #define した定数です。メモリはその場で確保され、あとから割り当てられるものは何もありません。
宣言したばかりのローカル配列の中身は、ゼロではなくゴミです:
2 行目はゼロを表示するかもしれませんし、巨大な数を表示するかもしれませんし、実行するたびに、あるいはデバッグビルドとリリースビルドとで違うかもしれません。これらの値を読むのは未定義動作です。読む前に初期化しましょう。
初期化
いくつもの書き方があり、どれも役に立ちます:
覚えておくべきは {0} のイディオムです。列挙しなかった要素はゼロ初期化されるので、ゼロを 1 つ書けば配列全体がゼロになります。指示付き初期化子([4] = 99)を使えば特定の位置だけを設定して残りをゼロのままにできるので、ほとんどが空のルックアップテーブルに便利です。
できないことが 1 つあります。あとから配列を代入することです:
int a[3] = {1, 2, 3};
int b[3];
b = a; // error: an array is not assignable
memcpy(b, a, sizeof a); // this is how you copy one
インデックスはゼロから始まる
最初の要素は arr[0] で、最後は arr[n-1] です。arr[n] は存在しません。
最後のループを実行して、アドレスを見てください。どれもひとつ前からちょうど sizeof(int) バイトだけ先にあります。要素は本当に端から端まで並んでいるのです:
インデックス 0 1 2 3 4
+--------+--------+--------+--------+--------+
値 | 88 | 95 | 75 | 60 | 100 |
+--------+--------+--------+--------+--------+
オフセット +0 +4 +8 +12 +16 バイト
^
&scores[0]、これは `scores` が減衰した先でもある
ゼロ始まりのインデックスは恣意的な選択ではありません。インデックスを先頭からのオフセットにするための選択であり、それこそがアドレス算術に必要なものです。scores[i] は *(scores + i) として定義されています。ポインタと配列を参照してください。
長さを求める
C言語は実行時に配列の長さをどこにも保持していませんが、コンパイラは知っており、sizeof を通じてそれに手が届きます:
sizeof(arr) / sizeof(arr[0]) が定番のイディオムです。ハードコードした sizeof(double) ではなく arr[0] で割ることで、要素の型を変えてもこの行はそのまま動き続けます。
罠はここです。これは配列が宣言された場所でしか動きません。配列を関数に渡すと仮引数はポインタになるので、sizeof は代わりにポインタを測ります。たいていは 8 バイトなので、「長さ」は 1 か 2 になってしまいます。C言語で配列を受け取る関数が必ず個数も受け取るのは、そのためです:
配列をループする
標準的な形は、0 から始めて i < n の間まわす for ループです:
i <= n ではなく i < n である点に注目してください。要素が n 個あるとき有効なインデックスは 0 から n - 1 までなので、<= にすると 1 回余分にまわって末尾の外を読んでしまいます。hottest と coldest を 0 ではなく temps[0] で初期化するのも、真似する価値のあるもう 1 つの習慣です。最大値をゼロから始めると、すべて負のデータで静かに壊れます。
範囲外アクセスは未定義動作
ここが、他の言語から来た人を驚かせる C言語の部分です。境界チェックはありません。コンパイル時にもなく、実行時にもありません。5 要素の配列に対する arr[10] はエラーではなく、コンパイラが黙って行うアドレス計算です。
int arr[5] = {1, 2, 3, 4, 5};
arr[7] = 99; // writes 8 bytes past the end of the array
int x = arr[-1]; // reads before the start
その次に起きるのは未定義動作で、その症状は困ったことにさまざまです:
- 動いているように見える。たまたま踏んだメモリが使われていなかったから。
- 別の変数が、目に見える理由もなく変わる。
- プログラムがセグメンテーション違反でクラッシュする。ずっとあとになってから起きることもある。
-O0と-O2とで挙動が変わる。
危険なのは「動いているように見える」です。それはバグが出荷されることを意味するからです。守り方は 3 つあります。
i < nでループする。 大半の範囲外アクセスは、ループ条件の off-by-one(1 つずれ)です。- 入力から来たインデックスを検証する。
- 開発中はサニタイザ付きでビルドする。
gcc -fsanitize=address -g prog.cとすれば、ほとんどの範囲外アクセスが即座に報告され、ファイル名・行番号・関係する配列の名前まで教えてくれます。
他の型の配列
同じ構文はどんな要素型にも使えます。構造体も含めてです:
char word[6] = "hello"; の行は立ち止まる価値があります。C言語の文字列とは、意味を持つ最後のバイトが '\0' である char の配列にすぎません。5 文字なのに 6 個分の枠が要るのは、その終端文字のためです。
可変長配列について簡単に
C99 では、サイズが実行時の値である配列が許されています:
int n = get_count();
int buffer[n]; // a variable-length array
注意が 2 つあります。サイズは配列を作った時点で固定されるので、VLA は伸びるリストではありません。そしてメモリはスタックから取られるので、n が大きい、あるいは攻撃者に操作できる値だとスタックをあふれさせてプログラムをクラッシュさせます。Linux カーネルを含む多くのプロジェクトが VLA を全面的に禁止しているのはこのためです。また、C11 の実装が VLA をサポートするかどうかは任意です。
サイズが実行時まで本当に分からない場合は、代わりに割り当てましょう:
arr が配列でも割り当てたメモリへのポインタでも、arr[i] はまったく同じように読める点に注目してください。インデックス指定の構文は気にしないのです。詳しくは動的メモリで扱います。
よくある質問
C言語で配列を宣言するには?
要素の型、名前、角括弧に入れたサイズを書きます。int scores[5]; は 5 つの int 分の領域を確保します。サイズは配列を宣言する場所で分かっている定数でなければならず(可変長配列を除く)、値を与えない限り要素は未初期化のままです。
C言語で配列の長さを求めるには?
sizeof(arr) / sizeof(arr[0]) です。全体のバイト数を 1 要素のサイズで割ります。これは配列が宣言されたスコープの中でしか動きません。関数に渡した時点で配列はポインタへ減衰しているので、sizeof は代わりにポインタを測ってしまいます。
C言語で配列の末尾を越えるとどうなりますか?
何も止めてくれません。C言語は境界チェックを行わないので、5 要素の配列に対する arr[10] はそこにあるメモリをそのまま読み書きします。これは未定義動作です。ゴミを表示したり、別の変数を壊したり、クラッシュしたりします。そして、壊れるまでは動いているように見えることもあります。
C言語で配列の全要素をゼロで初期化するには?
int arr[100] = {0}; です。列挙しなかった要素はゼロ初期化されるので、ゼロを 1 つ書くだけで配列全体が埋まります。C23 では int arr[100] = {}; も使えます。初期化子がまったくない場合、ローカル配列の中身は不定なゴミです。