Menu

C++ の配列:宣言、インデックス、よくある落とし穴

C++ の生配列を解説:宣言と初期化の方法、安全なインデックス指定、サイズを使ったループ、配列からポインタへの減衰という罠、そして std::array や vector が通常は優れている理由。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

固定長の値の並び

配列とは、同じ型の値を固定個数だけ保持する連続したメモリブロックです。単一の 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::arraystd::vector へと向かわせる摩擦です。これらは自身のサイズを保持し、決して減衰しません。

多次元配列について簡単に

角括弧を入れ子にすると格子を作れます。2 次元配列は実際には配列の配列で、メモリ上では行ごとに並べられます:

grid[row][col] の形でインデックス指定します。境界と減衰に関する同じ注意点が当てはまり、しかも悪化します。2 次元配列を関数に渡すには、最初を除くすべての次元を明示しなければならないからです(void f(int g[][3]))。小さな固定格子を超えるものなら、vectorvector のほうがはるかにミスが起きにくいです。

次へ: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 の範囲に収めてください。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める