固定長の値の並び
配列とは、同じ型の値を固定個数だけ保持する連続したメモリブロックです。単一の int が 1 つの数値を格納するのに対し、int の配列はそれらを背中合わせに多数格納し、それぞれに整数インデックスでアクセスできます。
配列を宣言するには、要素の型、名前、角括弧内のサイズを指定します。中身を埋めるには波括弧で囲んだリストを追加します:
インデックスはゼロから始まります。最初の要素は scores[0] で、サイズ 4 の配列の有効なインデックスは 0 から 3 までです。サイズはコンパイル時の定数でなければなりません。標準 C++ では int n = readInput(); int a[n]; とは書けません(それは移植性のない拡張です)。サイズを実行時に決めたい場合は、代わりに vector を使いましょう。
配列の初期化
配列を埋める方法はいくつかあり、知っておく価値のあるショートカットも 2 つほどあります:
身につけておくべき要点:サイズより少ない初期化子を与えると、残りはゴミではなく値初期化されます(数値型ではゼロ)。しかし初期化子がまったくない配列、つまりローカル変数として宣言した int e[4]; は不定値を含み、代入する前にそれらを読むのは未定義動作です。
配列をループする
要素は連続して格納されているので、配列は単純なインデックスループで走査します。範囲内に収めるには、ハードコードした数値ではなく配列の実際の長さでループを駆動しましょう:
sizeof(scores) は配列全体の合計バイト数で、これを sizeof(scores[0])(1 要素のサイズ)で割ると要素数が得られます。C++17 以降には、より読みやすい書き方 std::size(scores) があり、誤ってポインタを渡すとコンパイルを拒否してくれます。値だけが必要な場合はさらに簡単で、範囲ベースの for を使えばインデックス計算を完全に省けます。
範囲外アクセスの罠
C++ は arr[i] に対して境界チェックをまったく行いません。最後の要素を越えてインデックス指定しても例外も警告も出ず、たまたまそこにあるメモリを読み書きします。これは配列のバグとして最も多く、典型的な未定義動作です:
int a[3] = {1, 2, 3};
a[3] = 99; // OOPS - valid indices are 0..2, not 3
cout << a[5]; // garbage, crash, or corruption - undefined behavior
オフバイワンのミスはたいていループ条件に潜んでいます。i < n の代わりに i <= n と書くと 1 歩進みすぎて、存在しない arr[n] に触れてしまいます:
for (int i = 0; i <= n; i++) // BUG: when i == n, arr[i] is out of bounds
cout << arr[i];
解決策は前節の規律です。ループは <= ではなく必ず i < size とし、要素を追加すると食い違うリテラルを書き直すのではなく、サイズは配列から計算しましょう。
配列の減衰:隠れたポインタ
C++ で最も厄介な配列の挙動が**減衰(decay)**です。配列を関数に渡すと、それは静かに先頭要素へのポインタに変換されます。サイズ情報は失われるため、関数内の sizeof は配列ではなくポインタを測ります。
関数パラメータとしての int arr[] と int* arr は同一であることに注意してください。角括弧は見た目だけのものです。個数が失われるため、長さは配列とともに自分で渡さなければなりません:
int sum(const int* arr, int n) {
int total = 0;
for (int i = 0; i < n; i++) total += arr[i];
return total;
}
この「ポインタと長さを渡して、一致することを祈る」というパターンこそが、多くの C++ コードを std::array や std::vector へと向かわせる摩擦です。これらは自身のサイズを保持し、決して減衰しません。
多次元配列について簡単に
角括弧を入れ子にすると格子を作れます。2 次元配列は実際には配列の配列で、メモリ上では行ごとに並べられます:
grid[row][col] の形でインデックス指定します。境界と減衰に関する同じ注意点が当てはまり、しかも悪化します。2 次元配列を関数に渡すには、最初を除くすべての次元を明示しなければならないからです(void f(int g[][3]))。小さな固定格子を超えるものなら、vector の vector のほうがはるかにミスが起きにくいです。
次へ:Vector
生配列は高速で予測しやすいものの、固定サイズ、境界安全性の欠如、ポインタへの減衰という挙動により、日常的なコードでは扱いにくくなります。次は std::vector を見ていきます。これは必要に応じて伸びるリサイズ可能な配列で、自身のサイズを覚え、STL アルゴリズムにそのまま接続できます。配列が提供するほぼすべてを、自分の足を撃つ方法をはるかに減らした形で手に入れられます。
よくある質問
C++ で配列を宣言して初期化するには?
要素の型、名前、角括弧内のサイズを書き、必要に応じて波括弧のリストを続けます:int scores[4] = {90, 75, 100, 60};。初期化子を与える場合はサイズを省略できます。int scores[] = {90, 75, 100, 60}; とすれば、コンパイラが代わりに数を数えてくれます。
C++ で配列の長さを取得するには?
まだスコープ内にある本物の配列に対しては、std::size(arr)(C++17)または sizeof(arr) / sizeof(arr[0]) を使います。配列がポインタへ減衰したあと(たとえば int arr[] を受け取った関数の内部)では、これは動作しません。そこでは sizeof は配列ではなくポインタのサイズを返します。
C++ で配列に範囲外アクセスをするとどうなりますか?
未定義動作です。C++ は arr[i] に対して境界チェックを行わないため、末尾を越えて読み書きするとクラッシュしたり、ゴミ値が返ったり、近くのメモリを静かに破壊したりすることがあります。インデックスは常に 0 から size - 1 の範囲に収めてください。