C++の多次元配列:Cスタイル配列と std::array のネスト(入れ子)

1次元の配列はデータの「列」を表現しますが、実世界の多くのデータは「行」と「列」を持つ2次元の表(グリッド)として表現されます(例:スプレッドシート、画像ピクセル、ゲームのマップ)。

C++では、このような2次元、3次元、あるいはそれ以上の次元のデータを扱うために多次元配列を使用できます。これは本質的に「配列の配列」として構成されます。

目次

伝統的なCスタイル多次元配列

C言語から引き継がれた伝統的な多次元配列は、以下のように宣言します。

// 3行4列の int 型 2次元配列
int grid[3][4];

これは、「4個のint型配列」を3つ集めた配列、として解釈されます。宣言の順序は [行数][列数] となるのが一般的です。

メモリ上の並び順

多次元配列の要素は、メモリ上では連続した領域に配置されます。その際、末尾側の添字(インデックス)が最も速く増える順序で並びます。

grid[3][4] の場合、以下の順序でメモリに並びます。 grid[0][0], grid[0][1], grid[0][2], grid[0][3], grid[1][0], grid[1][1], grid[1][2], grid[1][3], grid[2][0], grid[2][1], grid[2][2], grid[2][3]

初期化と走査

初期化には、入れ子(ネスト)にした {} を使用します。 データの走査(全要素の処理)には、ネストした for ループを使用します。外側のループが行を、内側のループが列を処理するのが一般的です。

#include <iostream>

int main() {
    // 3行4列の座席予約状況 (1:予約済, 0:空席)
    int seatStatus[3][4] = {
        {1, 0, 1, 0}, // 0行目
        {0, 0, 1, 1}, // 1行目
        {1, 1, 0, 0}  // 2行目
    };

    std::cout << "座席の予約状況 (1:予約済, 0:空席):\n";
    
    // 外側のループ (行: i)
    for (int i = 0; i < 3; ++i) {
        // 内側のループ (列: j)
        for (int j = 0; j < 4; ++j) {
            std::cout << seatStatus[i][j] << " ";
        }
        std::cout << "\n"; // 1行終わったら改行
    }

    return 0;
}

Cスタイル多次元配列の問題点

Cスタイル配列は単純ですが、C++の観点からは多くの問題点を抱えています。

1. サイズの特定が困難 (sizeof トリック)

Cスタイル配列は、それ自体がサイズ情報(行数や列数)を保持していません。そのため、要素数を計算するために sizeof 演算子を使った、以下のような直感的でない「トリック」が必要になります。

#include <iostream>

int main() {
    int data[4][3]; // 4行3列

    // 1. 行数を求める (配列全体のサイズ / 1行分のサイズ)
    size_t rowCount = sizeof(data) / sizeof(data[0]);
    
    // 2. 列数を求める (1行分のサイズ / 1要素分のサイズ)
    size_t colCount = sizeof(data[0]) / sizeof(data[0][0]);
    
    // 3. 全要素数を求める (配列全体のサイズ / 1要素分のサイズ)
    size_t totalElements = sizeof(data) / sizeof(data[0][0]);

    std::cout << "行数: " << rowCount << "\n";      // 4
    std::cout << "列数: " << colCount << "\n";      // 3
    std::cout << "全要素数: " << totalElements << "\n"; // 12

    return 0;
}

この方法は非常に間違いやすく、配列が関数にポインタとして渡されると、この計算は破綻します。

2. 型情報の確認 (typeid)

typeid 演算子(<typeinfo> ヘッダ)で型を調べると、Cスタイル多次元配列が「配列の配列」であることが確認できます。

#include <iostream>
#include <typeinfo>

int main() {
    int a[5][3]; // 5行3列

    // 型情報の表示 (出力内容は処理系に依存)
    std::cout << "配列 a の型: " << typeid(a).name() << "\n";
    // 例: A5_A3_i (「3個のint配列」の「5個の配列」)
    
    std::cout << "a[0] の要素型: " << typeid(a[0]).name() << "\n";
    // 例: A3_i (「3個のint配列」)
    
    std::cout << "a[0][0] の要素型: " << typeid(a[0][0]).name() << "\n";
    // 例: i (int)

    return 0;
}

現代C++の推奨:std::array のネスト

C++11以降、Cスタイル配列の上記の問題点をすべて解決するために、<array> ヘッダが提供する std::array を使用することが強く推奨されます。

多次元配列は、std::array をネスト(入れ子)にすることで実現します。

std::array< std::array< 型, 列数 >, 行数 >

#include <iostream>
#include <array>    // std::array のために必要
#include <iomanip>  // std::setw のために必要

// 定数でサイズを定義すると管理が容易になる
constexpr size_t ROWS = 3; // 行数
constexpr size_t COLS = 4; // 列数

// 3行4列の座席予約状況 (std::array を使用)
// std::array<int, 4> (1行分) を 3つ (ROWS) 持つ配列
using SeatRow = std::array<int, COLS>;
using SeatGrid = std::array<SeatRow, ROWS>;

int main() {
    SeatGrid seatStatus = {{
        {1, 0, 1, 0}, // 0行目
        {0, 0, 1, 1}, // 1行目
        {1, 1, 0, 0}  // 2行目
    }};

    std::cout << "座席の予約状況 (O:予約済, X:空席):\n";
    
    // C++11以降の 範囲ベースfor文 をネストして使用
    // 外側のループ (行: row)
    for (const auto& row : seatStatus) {
        
        // 内側のループ (列: status)
        for (const auto& status : row) {
            char display = (status == 1) ? 'O' : 'X';
            std::cout << std::setw(3) << display;
        }
        std::cout << "\n"; // 1行終わったら改行
    }

    // サイズ取得が非常に簡単
    std::cout << "--- グリッド情報 ---\n";
    std::cout << "行数: " << seatStatus.size() << "\n";      // 3
    std::cout << "列数: " << seatStatus[0].size() << "\n";  // 4 (0行目の要素数)
    
    return 0;
}

実行結果:

座席の予約状況 (O:予約済, X:空席):
  O  X  O  X
  X  X  O  O
  O  O  X  X
--- グリッド情報 ---
行数: 3
列数: 4

std::array を使うメリット

std::array をネストして使用することには、Cスタイル配列と比較して計り知れないメリットがあります。

  1. サイズが明確: .size() メンバ関数で、sizeof トリックに頼ることなく、行数や列数を安全かつ直感的に取得できます。
  2. 安全な関数渡し: 関数に渡す際も、ポインタに「縮退」してサイズ情報が失われることがありません。
  3. コピーが可能: gridB = gridA; のように、代入演算子 = で全要素のコピーが可能です(Cスタイル配列では不可)。
  4. 標準ライブラリとの互換性: .begin(), .end() を持つため、C++の標準アルゴリズム(ソート、検索など)とシームレスに連携できます。

まとめ

C++で多次元配列を扱うことは可能ですが、C言語由来の伝統的な配列は、サイズ管理やコピーの面で多くの困難を伴います。

固定サイズの多次元配列が必要な場合、C++11以降では std::array をネスト(入れ子)にする方法が、最も安全で、可読性・保守性の高い現代的な解決策です。(もしサイズが実行時に変わる場合は std::vector<std::vector<int>> を使用します。)

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次