12.

C++の多次元配列をfor文で走査する方法|ネストforと範囲forの使い方

編集

C++で多次元配列をfor文で走査するには、行を回す外側のループの中に列を回す内側のループをネストし、array[i][j] のように各次元の添字を指定して全要素にアクセスします。範囲for文を使えば添字を書かずに走査することもできます。本記事では、二次元配列を中心に、宣言・ネストしたfor文での走査・範囲for文での走査・初期化・三次元以上への拡張・つまずきやすい落とし穴までをコード付きで整理します。

この記事の要点
  • 多次元配列は int a[3][4]; のように次元ごとに [ ] を並べて宣言する。
  • 走査は外側で行(第1添字)、内側で列(第2添字)を回すネストしたfor文が基本。
  • 添字が不要なら範囲for文for (auto& row : a) for (auto& v : row) と書ける。
  • 範囲for文では各行を参照(auto&で受けるのが安全。値で受けるとコピーが発生する。
  • C++の多次元配列はメモリ上に行優先(row-major)で連続配置される。
  • 要素数の扱いは別記事 多次元配列の要素数 / 配列の要素数 を参照。

 

多次元配列の宣言

多次元配列は、要素の型に続けて次元の数だけ [ ] を並べて宣言します。二次元配列であれば 型 名前[行数][列数]; という形になります。たとえば「3行4列」の int 型配列は次のように宣言します。

int a[3][4];   // 3行4列、計12要素

このとき、第1添字 i02(行)、第2添字 j03(列)の範囲を取り、各要素には a[i][j] でアクセスします。添字は 0 から始まる点に注意してください。a[3][4] のように宣言サイズと同じ添字を指定すると範囲外アクセスになります。

各次元の要素数(上の例では 34)の数え方や取得方法については、関連記事 多次元配列の要素数 および 配列の要素数 で詳しく扱っています。本記事では走査に集中します。

 

ネストしたfor文で走査する

二次元配列を走査する最も基本的な方法は、for文を二重にネストすることです。外側のループで行(第1添字 i)を回し、内側のループで列(第2添字 j)を回します。次の例は3行4列の配列に値を入れ、全要素を行ごとに出力します。

#include <iostream>

int main() {

    const int rows = 3;
    const int cols = 4;

    int a[rows][cols];

    // 値を代入(行優先で連番を入れる)

    for (int i = 0; i < rows; ++i) {     // 行ループ
       
for (int j = 0; j < cols; ++j) {  // 列ループ
            a[i][j] = i * cols + j;
        }
    }

    // 走査して出力

    for (int i = 0; i < rows; ++i) {
       
for (int j = 0; j < cols; ++j) {
            std::cout << a[i][j] << ' ';
        }
        std::cout << '\n';  // 1行ぶん出力したら改行
    }
}

出力は次のようになります。

0 1 2 3

4 5 6 7

8 9 10 11

内側のループが終わるたびに改行を入れることで、配列の形(3行4列)どおりに表示されます。要素を一つずつ処理したいだけなら改行は不要で、a[i][j] の中身を目的の処理に置き換えれば、合計を求めたり最大値を探したりといった処理にそのまま応用できます。

 

範囲for文で走査する

添字 ij を自分で管理したくない場合は、範囲for文(range-based for)を使うと簡潔に書けます。二次元配列は「行(一次元配列)の配列」とみなせるため、外側で各行を取り出し、内側でその行の各要素を取り出す、という二段構えになります。

#include <iostream>

int main() {

    int a[3][4] = {
        {0, 1, 2, 3},
        {4, 5, 6, 7},
        {8, 9, 10, 11}
    };

    for (auto& row : a) {     // 各行を参照で受ける
       
for (auto& v : row) {  // 行内の各要素を参照で受ける
            std::cout << v << ' ';
        }
        std::cout << '\n';
    }
}

出力はネストしたfor文の例と同じく 0 1 2 3 / 4 5 6 7 / 8 9 10 11 になります。

ここで重要なのは、各行を auto& row のように参照で受けることです。二次元配列の「行」自体が配列(上の例では int[4])であるため、auto row と値で受けようとすると配列はコピーできず、内側の範囲for文がうまく動きません。要素の値を書き換えたい場合も、参照で受けていないと元の配列に反映されません。読み取り専用で書き換えないことを明示したいときは const auto& row とすると安全です。

 

多次元配列の初期化

宣言と同時に値を与えるには、波かっこ { } を入れ子にして「行ごと」に並べます。内側の { } が1行分に対応します。

// 行ごとに波かっこで初期化
int a[2][3] = {
    {1, 2, 3},
    {4, 5, 6}
};

// 全要素を0で初期化
int b[2][3] = {};

// 先頭だけ指定し、残りは0で埋まる
int c[2][3] = { {1, 2} };  // c[0][2] と c[1][*] は 0

初期化子の数が宣言サイズより少ない場合、足りない要素は 0 で初期化されます。int b[2][3] = {}; のように空の波かっこを書けば全要素が 0 になります。なお、最初の次元(行数)は初期化子から推測できるため int a[][3] = {...}; と省略できますが、2番目以降の次元は省略できません

 

三次元以上の配列を走査する

次元が増えても考え方は同じで、次元の数だけfor文をネストします。三次元配列なら外側から順に「面(第1添字)→行(第2添字)→列(第3添字)」を回します。

#include <iostream>

int main() {

    int a[2][2][2] = {
        { {1, 2}, {3, 4} },
        { {5, 6}, {7, 8} }
    };

    // ネストしたfor文(3重)
    for (int i = 0; i < 2; ++i)
        for (int j = 0; j < 2; ++j)
            for (int k = 0; k < 2; ++k)
                std::cout << a[i][j][k] << ' ';

    std::cout << '\n';

    // 範囲for文(3重)。各段を参照で受ける
    for (auto
& plane : a)
        for (auto
& row : plane)
            for (auto
& v : row)
                std::cout << v << ' ';
}

どちらの走査でも 1 2 3 4 5 6 7 8 の順で出力されます。次元が深くなるほどネストも深くなるため可読性は下がります。実務では三次元を超えるような場合、std::vectorstd::array を組み合わせたり、一次元配列に添字計算でマッピングしたりする設計も検討するとよいでしょう。

 

多次元配列とfor文の落とし穴

つまずきやすいポイント
  • 添字の範囲外アクセスint a[3][4] なら有効な添字は行 02、列 03。ループ条件を i <= 3 などと書くと範囲外になり、未定義動作になる。
  • 行と列の取り違えa[i][j]i は行、j は列。外側ループと内側ループ、ループ上限(rows / cols)の対応を取り違えやすい。
  • 範囲for文を値で受けるfor (auto row : a) のように値で受けると、行(配列)のコピーが意図どおり行えず期待した走査にならない。各行は auto& row(参照)で受けるのが基本。
  • メモリ配置の誤解:C++の多次元配列は行優先(row-major)で連続配置され、同じ行の要素がメモリ上で隣り合う。最も内側のループで列(最後の添字)を回すと連続アドレスを順にたどることになり、キャッシュ効率の面でも素直。

特にメモリ配置については、a[i][j]j(最後の添字)を変えると隣接アドレスへ移動し、i を変えると cols 個ぶん飛んだ位置へ移動します。つまり外側で行、内側で列を回す順序が、メモリ上の連続したならびに沿った走査になります。これは正しさには影響しませんが、大きな配列を扱うときの性能差として現れることがあります。

 

よくある質問(FAQ)

Q. 範囲for文の row はなぜ参照(&)で受けるのですか?
A. 二次元配列の各「行」は配列(例:int[4])そのものだからです。配列は値としてコピーできないため、auto row と値で受けると内側の走査が意図どおり動きません。参照 auto& row で受ければ元の行をそのまま参照でき、要素の書き換えも反映されます。書き換えないなら const auto& row が安全です。

Q. ネストしたfor文と範囲for文はどちらを使うべきですか?
A. 添字 ij 自体を計算に使う場合(位置に応じた処理、隣接要素の参照など)はネストしたfor文が向きます。単に全要素を順に処理したいだけなら、添字管理が不要で範囲外アクセスの心配も減る範囲for文のほうが簡潔で安全です。

Q. 各次元の要素数を for文の条件に直接書くにはどうすればよいですか?
A. rows / cols のような定数を用意してループ条件に使うのが分かりやすい方法です。配列から要素数を求める方法は本記事の範囲を超えるため、多次元配列の要素数配列の要素数 を参照してください。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. コメントアウト
  2. 文字列の結合/連結
  3. 変数の宣言
  4. 定数の宣言
  5. if文
  6. if文の論理演算子
  7. for文
  8. データ型(文字列以外)
  9. データ型(文字列)
  10. 配列とfor文
  11. 配列の要素数
  12. 多次元配列とfor文
  13. 多次元配列の要素数
  14. 関数の定義と呼び出し

最近更新/作成されたページ