多次元配列 | Programming Place Plus 新C++編

トップページ新C++編

先頭へ戻る

このページの概要

このページでは、多次元配列を取り上げます。多次元配列を使うと、要素が縦横に並んだ「表」のようなかたちをしているデータをうまく表現できます。「多次元」なので、縦横という2次元に限定されず、次元を増やしていくことも可能です(3次元を越えるとイメージすることが難しくなりすぎますが)。生の配列による方法を取り上げたあと、std::vector を使う方法にも触れます。

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



次のテーマ(ペイントスクリプト)

このページからは、再び新しいテーマを設定して進めていきます。次のテーマは「ペイントスクリプト」です。スクリプト (script) とは、コマンドを書き並べたファイルなどのことをいいます。専用のプログラムに渡してやると、そこに書かれているコマンドを解読しながら、そのコマンドに応じた処理が実行されていきます。今回はそうやって、色や座標を指示しながら、簡単な図形を描かせて、1枚の画像を作り上げるという内容です。

たとえば、次のように書いたテキストファイルを用意します。

fill 255 255 255
pen 255 0 0
line 100 100 200 200
pen 0 0 255
line 200 100 100 200
save test.bmp

「fill」は画像全体を塗りつぶすコマンドです。続く「255 255 255」は色を表現したものであり、光の三原色である「赤 緑 青」の強さを 0~255 の範囲で表現したものです。色をこのように表現することはとても一般的で、RGB形式などと呼ばれます。

「pen」は図形を描くために使うペンを指定するコマンドで、ここでも色を指定します。

「line」で直線を描きます。ここでは、線の2つの端点の座標を指定します。「100 100 200 200」なら、片側の点が (100,100) の位置にあり、もう一方の点が (200,200) にあることを示しています。色は、最後に「pen」で指定したものが使われます。

「save」によって、ここまでの実行結果をビットマップ画像 (bitmap image) として、拡張子が .bmp のファイルに出力します。

このような一連のコマンドを解読しながら実行していくことで、最終的には、赤と青の直線が斜めにクロスした画像が作られ、test.bmp として出力されるという動作を目指します。

多次元配列

ペイントスクリプトを作るにあたって、スクリプトを読み取る部分にはそれほど大きな障害はありません。これまでのページの知識だけでも何とかなるはずです。現状、はっきりと知識が足らなさそうなのは、ビットマップ画像を作る部分です。

ビットマップ画像は、画像内の点(ピクセル (pixel))を縦横に敷き詰めたものです。一方向に並べるだけであれば、これまでのページで使ってきた配列そのものですが、今回求められているのは「縦横に」並べることです。つまり「表」のような構造が必要なわけですが、表の横方向(行)だけに注目すれば、それは配列であることが分かります。「行」を縦方向に並べていけば全体として「表」になります。つまり、配列を配列のように並べればいいということになります。このような構造を、二次元配列 (2-dimensional array) と呼びます。

この考え方はさらに拡張できて、横にも縦にも奥にも並んでいれば三次元配列です。これ以上はイメージすることが難しいですが、四次元配列でも五次元配列でも可能です。二次元以上の配列のことをまとめて、多次元配列 (multi dimensional array) と呼びます。また、多次元配列でない、一番シンプルな配列を一次元配列 (1-dimensional array) と呼ぶことがあります。

一応、四次元配列は三次元配列が多数集まっているさまを、五次元配列は四次元配列が多数集まっているさまを考えればイメージできますが、プログラムを正しく読み書きすることはなかなか大変です。よほどの理由がないかぎり、三次元配列までにとどめておきましょう。

間違えてはいけないのは、二次元配列の要素が縦横に並ぶというのはイメージの話に過ぎないということです。メモリ上では一次元配列と同様、連続するメモリアドレスに要素が隙間なく並んでいます。プログラマーは二次元のイメージで考えてプログラミングすればいいですが、メモリ上でどうなっているかを意識しなければならない場面が巡ってきたときには、一次元的に並べられているさまをイメージしなければなりません。


ところで、これまでにも多次元配列が登場したことがあります。たとえば、std::string も std::vector も一次元配列とみなせますから、std::vector<std::string> は二次元配列になっているといえます。コマンドライン引数(「コマンドライン引数」のページを参照)も文字列が複数個あるわけですから、一次元配列が並んだ二次元配列のかたちを取っています。つまり、std::vector や std::string を使った実現もでき、そのほうが安全で便利です。ただし仕組み上、メモリの使用量が増える可能性が高いです

このページではまず、生の配列による多次元配列を説明し、最後に std::vector による多次元配列を取り上げます。

多次元配列を定義する

二次元配列は、次のように定義します。

型名 名前[要素数][要素数];
型名 名前[要素数][要素数] 初期化子;
型名 名前[][要素数] 初期化子;

たとえば、int array[10][5]; のように書きます。この場合 array は要素数10 の配列であり、その要素が、要素数 5 の配列であることを意味します。つまりこれは「配列の配列」であるといえます。

「初期化子」の指定があれば、1つ目の「要素数」に関してのみ指定を省略できます。その場合は、一次元配列のときと同様、初期化子から要素数が判断されます。

三次元配列では要素数の指定がさらにもう1つ増えます。四次元以上でも同様に、次元の数だけ要素数を指定します。

型名 名前[要素数][要素数][要素数];
型名 名前[要素数][要素数][要素数] 初期化子;
型名 名前[][要素数][要素数] 初期化子;

省略できる「要素数」はつねに先頭の1つだけです。2つ以上を省略すると、どのようなかたちをした多次元配列なのかを判断できないためです。


「初期化子」の書き方には2つの考え方があります。1つには、「配列の配列」であることを意識して、各配列を初期化していく考え方です。この方法は表のように初期値が並ぶので、とても分かりやすく、大抵はこの方法を使うことになります。

int array[3][5] {
    {  0,  1,  2,  3,  4},  // array[0] にあたる配列に対する初期化子
    { 10, 11, 12, 13, 14},  // array[1] にあたる配列に対する初期化子
    { 20, 21, 22, 23, 24},  // array[2] にあたる配列に対する初期化子
};

// 1つ目の要素数を省略しても、初期化子から判断できる
int array[][5] {
    {  0,  1,  2,  3,  4},  // array[0] にあたる配列に対する初期化子
    { 10, 11, 12, 13, 14},  // array[1] にあたる配列に対する初期化子
    { 20, 21, 22, 23, 24},  // array[2] にあたる配列に対する初期化子
};

三次元配列なら次のようになります。

int array[3][5][7] {
    // array[0] にあたる二次元配列に対する初期化子
    {
        {   0,   1,   2,   3,   4,   5,   6},  // array[0][0] にあたる配列に対する初期化子
        {  10,  11,  12,  13,  14,  15,  16},  // array[0][1] にあたる配列に対する初期化子
        {  20,  21,  22,  23,  24,  25,  26},  // array[0][2] にあたる配列に対する初期化子
        {  30,  31,  32,  33,  34,  35,  36},  // array[0][3] にあたる配列に対する初期化子
        {  40,  41,  42,  43,  44,  45,  46},  // array[0][4] にあたる配列に対する初期化子
    },
    // array[1] にあたる二次元配列に対する初期化子
    {
        { 100, 101, 102, 103, 104, 105, 106},  // array[1][0] にあたる配列に対する初期化子
        { 110, 111, 112, 113, 114, 115, 116},  // array[1][1] にあたる配列に対する初期化子
        { 120, 121, 122, 123, 124, 125, 126},  // array[1][2] にあたる配列に対する初期化子
        { 130, 131, 132, 133, 134, 135, 136},  // array[1][3] にあたる配列に対する初期化子
        { 140, 141, 142, 143, 144, 145, 146},  // array[1][4] にあたる配列に対する初期化子
    },
    // array[2] にあたる二次元配列に対する初期化子
    {
        { 200, 201, 202, 203, 204, 205, 206},  // array[2][0] にあたる配列に対する初期化子
        { 210, 211, 212, 213, 214, 215, 216},  // array[2][1] にあたる配列に対する初期化子
        { 220, 221, 222, 223, 224, 225, 226},  // array[2][2] にあたる配列に対する初期化子
        { 230, 231, 232, 233, 234, 235, 236},  // array[2][3] にあたる配列に対する初期化子
        { 240, 241, 242, 243, 244, 245, 246},  // array[2][4] にあたる配列に対する初期化子
    },
};

もう1つの考え方は、メモリ上では一次元的に並んでいるという事実を意識して、初期値も一次元的に並べてしまう方法です。

int array[3][5] {0, 1, 2, 3, 4, 10, 11, 12, 13, 14, 20, 21, 22, 23, 24};

いずれの方法を使っても、初期化子の個数が足らなければその部分に対応する要素は値初期化(「構造体」のページを参照)されます。

int array[3][5] {
    { 0, 1, 2, 3, 4},
    { 10, 11, 12},     // array[1][3] と array[1][4] は値初期化
                       // array[2] の各要素は値初期化
};

要素へのアクセス

要素へのアクセスは、添字演算子([])を使えばいいですが、次元の個数だけ書き並べる必要があります。もはや言うまでもないことですが、配列の範囲外を指定することは未定義の動作です

次のプログラムは、二次元配列のすべての要素の値の合計を出力しています。

#include <iostream>

int main()
{
    int array[3][5] {
        {  0,  1,  2,  3,  4},
        { 10, 11, 12, 13, 14},
        { 20, 21, 22, 23, 24},
    };
    
    int sum {0};
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 5; ++j) {
            sum += array[i][j];
        }
    }
    std::cout << sum << "\n";
}

実行結果:

180

すべての要素にアクセスできればいいので、2つの for文が値を変化させる変数を入れ替えて、次のように書くことも考えられます。

for (int j = 0; j < 5; ++j) {
    for (int i = 0; i < 3; ++i) {
        sum += array[i][j];
    }
}

実行結果:

180

しかし、実行効率の観点からいえば、最初の方法が優れている可能性があります。

配列 array の要素はメモリ上で「0, 1, 2, 3, 4, 10, 11, 12, 13, 14, 20, 21, 22, 23, 24」の順番に並んでいます。この並びのとおりにアクセスしていくことが一番効率的です。最初の方法はまさにこの順番でアクセスしています。これに対し、2つ目の方法では「0, 10, 20, 1, 11, 21, 2, 12, 22, 3, 13, 23, 4, 14, 24」の順番にアクセスしていますから、メモリ上をあちこち飛び回っていることになります。

詳細な解説は省きますが、これはキャッシュメモリという高速化の仕組みによるものです。配列が小さければ差はありませんが、大きい場合には違いが出てくる可能性があります。

要素数を取得する

生の一次元配列の要素数は sizeof(配列) / sizeof(要素の型) で計算できました、また、これをマクロにする方法も取り上げました(「配列」のページを参照)。

#define SIZE_OF_ARRAY(array)  (sizeof(array) / sizeof(array[0]))
auto size = SIZE_OF_ARRAY(array);  // array の要素数

多次元配列の場合でも考え方は同じですが、どのような意味になるのかはよく理解しなければなりません。

#include <iostream>

#define SIZE_OF_ARRAY(array)  (sizeof(array) / sizeof(array[0]))

int main()
{
    int array[3][5] {
        {  0,  1,  2,  3,  4},
        { 10, 11, 12, 13, 14},
        { 20, 21, 22, 23, 24},
    };

    std::cout << SIZE_OF_ARRAY(array) << "\n";
}

実行結果:

3

この場合、結果は 15 ではなく 3 です。この結果は、二次元配列が「配列の配列」であることを理解していれば納得できるはずです。array という配列の要素数を調べたわけですから、その要素数はあくまでも 3個なのです(その1個1個が、要素数 5 の配列である)。

ここで欲しい結果が 15 なのだとするなら、SIZE_OF_ARRAYマクロの結果に、array[0] に含まれている要素数を掛け合わさなければなりません。sizeof(array[0]) / sizeof(array[0][0]) の結果を掛け合わせればいいということです。

#include <iostream>

// 二次元配列の要素数を調べる
#define SIZE_OF_2DIM_ARRAY(array)  (sizeof(array) / sizeof(array[0]) * sizeof(array[0]) / sizeof(array[0][0]))

int main()
{
    int array[3][5] {
        {  0,  1,  2,  3,  4},
        { 10, 11, 12, 13, 14},
        { 20, 21, 22, 23, 24},
    };

    std::cout << SIZE_OF_2DIM_ARRAY(array) << "\n";
}

実行結果:

15

sizeof(array[0]) / sizeof(array[0][0]) の部分は、二次元配列の2つ目の次元に含まれる要素数を計算しているわけですが、これはこれで、二重の for文を使うときなどに有用なので、マクロを作っておくのもいいでしょう。

#include <iostream>

// 一次元配列の要素数を調べる
#define SIZE_OF_ARRAY(array)        (sizeof(array) / sizeof(array[0]))

// 二次元配列の2つ目の次元の要素数を調べる
#define SIZE_OF_ARRAY2(array)       (sizeof(array[0]) / sizeof(array[0][0]))

// 二次元配列の要素数を調べる
#define SIZE_OF_2DIM_ARRAY(array)   (SIZE_OF_ARRAY(array) * SIZE_OF_ARRAY2(array))

int main()
{
    int array[3][5] {
        {  0,  1,  2,  3,  4},
        { 10, 11, 12, 13, 14},
        { 20, 21, 22, 23, 24},
    };

    for (std::size_t i = 0; i < SIZE_OF_ARRAY(array); ++i) {
        for (std::size_t j = 0; j < SIZE_OF_ARRAY2(array); ++j) {
            std::cout << array[i][j] << " ";
        }
        std::cout << "\n";
    }
}

実行結果:

0 1 2 3 4
10 11 12 13 14
20 21 22 23 24

ポインタへの変換

配列は暗黙的にポインタに変換されるルールがありました(「配列とポインタ」のページを参照)。多次元配列(配列の配列)にもこのルールは存在しており、「配列の配列」は「配列へのポインタ」に変換されます。勘違いしやすいところですが、「配列の配列」は「ポインタへのポインタ」には変換されません

int array[3][5] {
    {  0,  1,  2,  3,  4},
    { 10, 11, 12, 13, 14},
    { 20, 21, 22, 23, 24},
};

int (*p)[5] {array};   // OK. array[0] を指すポインタ
int** p2 {array};      // コンパイルエラー

int array[3][5] から暗黙的に変換した結果は、「要素数5 の int型配列を指すポインタ」です。これを型として記述すると、int (*)[5] となります。この型の変数p を定義するには、int (*p)[5] と書きます。分かりづらいので、auto に任せるのも手です。

auto p = array;

一方、int** で受け取ろうとしている行はコンパイルできません。たまにキャストして乗り切ろうとするコードを見かけますが、そもそもこれは間違っています。


const を付ける場合は、以下のように書きます。

int (* const p)[5] {array};   // p を書き換えられない

const int (*p)[5] {array};    // p が指し示す先にあるものを書き換えられない
int const (*p)[5] {array};    // 同上

const int (* const p)[5] {array};  // 組み合わせ


parray[0]、つまり各行の配列を指しています。p が指し示す位置を動かしていけば、二次元配列の各行にアクセスできます。

#include <iostream>

int main()
{
    int array[3][5] {
        {  0,  1,  2,  3,  4},
        { 10, 11, 12, 13, 14},
        { 20, 21, 22, 23, 24},
    };

    int (*p)[5] {array};  // array[0] を指すポインタ

    std::cout << (*p)[2] << "\n";  // array[0][2]
    ++p;                           // array[1]
    std::cout << (*p)[2] << "\n";  // array[1][2]
    ++p;                           // array[2]
    std::cout << (*p)[2] << "\n";  // array[2][2]
}

実行結果:

2
12
22

範囲for文

二次元配列の各要素を辿るために範囲for文を使うことも可能です。

#include <iostream>

int main()
{
    int array[3][5] {
        {  0,  1,  2,  3,  4},
        { 10, 11, 12, 13, 14},
        { 20, 21, 22, 23, 24},
    };
    
    int sum {0};
    for (auto& row : array) {
        for (auto elem : row) {
            sum += elem;
        }
    }
    std::cout << sum << "\n";
}

実行結果:

180

for (auto& row : array) のところは、auto ではなく auto& でなければなりません。array の各要素の型は int (*)[5] ですから、row の型を auto にしたのなら、int (*)[5] に型推論されることになります。その場合、内側の範囲for文 for (auto elem : row) のところでコンパイルエラーになります。なぜなら、row は配列ではなく、(配列を指す)ポインタだからです。範囲for文は、たとえ指している先にあるものが配列であっても、ポインタを指定することはできません。

一方、rowauto& にすると、要素数5 の int型配列を指す参照型に型推論されます。型名としては int (&)[5] です。参照型は、指しているものの別名として動作するので、これは配列そのものとみなせますから、内側の範囲for文もコンパイルできます。

多次元配列を関数に渡す

一次元配列を関数に渡そうとするとポインタに変換されてしまいますが(「配列とポインタ」のページを参照)、この動作はもちろん多次元配列でも同じです。そのため、次のプログラムは想定した動作になりません。

#include <iostream>

#define SIZE_OF_ARRAY(array)        (sizeof(array) / sizeof(array[0]))
#define SIZE_OF_ARRAY2(array)       (sizeof(array[0]) / sizeof(array[0][0]))

void print_multi_array(int array[3][5])  // 一見、要素数をしっかり指定できているようにみえるが・・
{
    for (std::size_t i {0}; i < SIZE_OF_ARRAY(array); ++i) {  // 正しくない
        for (std::size_t j {0}; j < SIZE_OF_ARRAY2(array); ++j) {  // こちらは正しい
            std::cout << array[i][j] << " ";
        }
        std::cout << "\n";
    }
}

int main()
{
    int array[3][5] {
        {  0,  1,  2,  3,  4},
        { 10, 11, 12, 13, 14},
        { 20, 21, 22, 23, 24},
    };

    print_multi_array(array);
}

実行結果:

さきほどの項で説明したルールのとおりで、仮引数の int array[3][5] という記述は int (*array)[5] と同じ意味になってしまいます3 と書いたのにも関わらず、その情報は活かされません。実際、渡す二次元配列を int array[10][5] に変えてみても、黙ってコンパイルされます。

仮引数の array は「要素数 5 の配列へのポインタ」の意味になってしまうので、SIZE_OF_ARRAY(array) が問題になります。SIZE_OF_ARRAYマクロの置換結果にあらわれる sizeof(array) はポインタ1個分のバイト数であり、sizeof(array[0]) は「要素数5 の配列の大きさ」です。ポインタ型や int型が 4バイトであるなら「4 / 20」という計算をしていることになるのです。

このように、それなりに知識を持っていないと誤解を与える記述になってしまうので、仮引数には「配列へのポインタ」であることが明確であるように書いたほうがいいでしょう。int (*array)[5] と書くのも良いですし、int array[][5] とするのも手です。

#include <iostream>

#define SIZE_OF_ARRAY(array)        (sizeof(array) / sizeof(array[0]))
#define SIZE_OF_ARRAY2(array)       (sizeof(array[0]) / sizeof(array[0][0]))

void print_multi_array(const int (*array)[5])  // 仮引数は int array[][5] でもいい
{
    for (std::size_t i {0}; i < 3; ++i) {
        for (std::size_t j {0}; j < SIZE_OF_ARRAY2(array); ++j) {
            std::cout << array[i][j] << " ";
        }
        std::cout << "\n";
    }
}

int main()
{
    int array[3][5] {
        {  0,  1,  2,  3,  4},
        { 10, 11, 12, 13, 14},
        { 20, 21, 22, 23, 24},
    };

    print_multi_array(array);
}

実行結果:

0 1 2 3 4
10 11 12 13 14
20 21 22 23 24

これでコンパイルが通り、動作も正常になります。

しかし、print_multi_array関数内で行数を 3 と指定せざるを得ないことは問題がありそうです。一次元配列のときにやっていたように、呼び出し元から情報を渡してもらうことで解決できます。

void print_multi_array(const int (*array)[5], std::size_t row_size)
{
    for (std::size_t i {0}; i < row_size; ++i) {
        for (std::size_t j {0}; j < SIZE_OF_ARRAY2(array); ++j) {
            std::cout << array[i][j] << " ";
        }
        std::cout << "\n";
    }
}

print_multi_array(array, SIZE_OF_ARRAY(array));

少し面倒になった反面で、行数に関しては自由度が生まれたという捉え方もできます。print_multi_array関数に渡す二次元配列の要素数は、10x5 でも 1x5 でもよくなっています。

#include <iostream>

#define SIZE_OF_ARRAY(array)        (sizeof(array) / sizeof(array[0]))
#define SIZE_OF_ARRAY2(array)       (sizeof(array[0]) / sizeof(array[0][0]))

void print_multi_array(const int (*array)[5], std::size_t row_size)
{
    for (std::size_t i {0}; i < row_size; ++i) {
        for (std::size_t j {0}; j < SIZE_OF_ARRAY2(array); ++j) {
            std::cout << array[i][j] << " ";
        }
        std::cout << "\n";
    }
}

int main()
{
    int array[10][5] {
        {  0,  1,  2,  3,  4},
        { 10, 11, 12, 13, 14},
        { 20, 21, 22, 23, 24},
        { 30, 31, 32, 33, 34},
        { 40, 41, 42, 43, 44},
        { 50, 51, 52, 53, 54},
        { 60, 61, 62, 63, 64},
        { 70, 71, 72, 73, 74},
        { 80, 81, 82, 83, 84},
        { 90, 91, 92, 93, 94},
    };

    print_multi_array(array, SIZE_OF_ARRAY(array));
}

実行結果:

0 1 2 3 4
10 11 12 13 14
20 21 22 23 24
30 31 32 33 34
40 41 42 43 44
50 51 52 53 54
60 61 62 63 64
70 71 72 73 74
80 81 82 83 84
90 91 92 93 94

こうなると列数のほうにも自由度を求めたくなって、次のようなコードを試みるかもしれませんが、これはコンパイルエラーになります。

void print_multi_array(const int array[][], std::size_t row_size, std::size_t col_size)
{
    for (std::size_t i {0}; i < row_size; ++i) {
        for (std::size_t j {0}; j < col_size; ++j) {
            std::cout << array[i][j] << " ";
        }
        std::cout << "\n";
    }
}

print_multi_array(array, SIZE_OF_ARRAY(array), SIZE_OF_ARRAY2(array));

問題になるのは仮引数の指定です。int array[][] とか int (*array)[] のように色々と書き方を探すものの、実はどれもうまくいきません。「多次元配列を定義する」の項で取り上げたとおり、宣言時に要素数の指定を省略できるのは最初の1つだけです。複数の次元に要素数の省略があると、どんなかたちの多次元配列であるのか決定できないためです。

行と列の両方の要素数を自由にしたければ発想を変える必要があります。多次元配列といえども、メモリ上では一次元的に隙間なく要素が並べられているという基本に立ち返るのです。つまり、あえて一次元配列のように取り扱うということです。

#include <iostream>

#define SIZE_OF_ARRAY(array)        (sizeof(array) / sizeof(array[0]))
#define SIZE_OF_ARRAY2(array)       (sizeof(array[0]) / sizeof(array[0][0]))

void print_multi_array(const int* array, std::size_t row_size, std::size_t col_size)
{
    for (std::size_t i {0}; i < row_size; ++i) {
        for (std::size_t j {0}; j < col_size; ++j) {
            std::cout << array[i * col_size + j] << " ";
        }
        std::cout << "\n";
    }
}

int main()
{
    int array[4][3] {
        {  0,  1,  2},
        { 10, 11, 12},
        { 20, 21, 22},
        { 30, 31, 32},
    };

    print_multi_array(&array[0][0], SIZE_OF_ARRAY(array), SIZE_OF_ARRAY2(array));
}

実行結果:

0 1 2
10 11 12
20 21 22
30 31 32

仮引数を int* array に変更しています。ただのポインタになったので、渡すべきものは多次元配列(実際には、配列へのポインタ)ではなく、多次元配列の先頭のメモリアドレスに変更しなければなりません。したがって、実引数は &array[0][0] になりました。一次元配列a に対する &a[0] の意味で a と書けることと同じルールによって、&array[0][0]array[0] と書いても構いません。

そして、print_multi_array関数の内側で、要素にアクセスするときの添字が i * col_size + j という計算式に置き換わっています。二次元配列はメモリ上で一次元的に並んでいるので、「行番号 * 列数 + 列番号」という計算によって、要素の位置を割り出せます。

多次元配列を関数から返す

さきほどとは反対に、関数から多次元配列を返す場合ですが、これは過去にも何度か書いているとおり、基本的に避けなければならないことです。「配列とポインタ」のページでの説明と同じことなので、ここでは割愛します。

必要であれば std::vector を使うといいでしょう。

std::vector による多次元配列

生の配列による多次元配列はかなり分かりづらく、間違いやすいものです。std::vector や std::string なら、要素数を自身で管理でき、イテレータや範囲for文を素直に使えて便利ですし、添字によるアクセスが必要なときにも、atメンバ関数を使う防御策が取れるので安全性が高くなります。

さきほど取り上げた、二次元配列を渡すと要素が出力される関数は、std::vector を使えば次のようにシンプルに書けます。

#include <iostream>
#include <vector>

void print_multi_array(const std::vector<std::vector<int>>& array)
{
    for (const auto& row : array) {
        for (auto elem : row) {
            std::cout << elem << " ";
        }
        std::cout << "\n";
    }
}

int main()
{
    std::vector<std::vector<int>> array {
        {  0,  1,  2},
        { 10, 11, 12},
        { 20, 21, 22},
        { 30, 31, 32},
    };

    print_multi_array(array);
}

実行結果:

0 1 2
10 11 12
20 21 22
30 31 32

要素数を変更する (resizeメンバ関数)

std::vector を使うことで、要素数を変更可能になる利点も生まれます。たとえば「要素を追加する」のページや「要素を取り除く」のページで説明した方法が使えます。ここではさらに、直接的に要素数を指定して変更する resizeメンバ関数1を紹介します。

省きますが、std::string にも resizeメンバ関数2があって、同じように使えます。

resizeメンバ関数は、指定した数になるように要素数を変更します。2つ目の引数を指定した場合は、要素の追加された場合にその値のコピーを使って初期化されます。

std::vector<int> vec {};

vec.resize(100);      // 要素数を 100 にする。追加された要素は値初期化
vec.resize(500, -1);  // 要素数を 500 にする。追加された要素は、第2引数をコピーしたもので初期化

指定している値は変更後の要素数なので、要素が増える方向に働くことも、減る方向に働くことも、あるいは変わらない可能性もあります。要素が増える場合、第2引数があればその値をコピーしたもので初期化され、第2引数がなければ値初期化(「std::vector」のページを参照)されます。要素が減る場合は、末尾側にある余分な要素が取り除かれます。

また、要素数と容量は別であることを思い出してください(「要素を追加する」)。resizeメンバ関数で指定した要素数よりも、実際に確保されているメモリの量は大きい可能性があります。


このページの本題であるペイントスクリプトでは、画像の記憶場所(絵を描く場所ということで、今後はキャンバスと呼びます)の大きさを、スクリプトから決めることに利用できます。

#include <iostream>
#include <vector>

struct Color {
    unsigned char red;          // 赤成分
    unsigned char green;        // 緑成分
    unsigned char blue;         // 青成分
};

using canvas_t = std::vector<std::vector<Color>>;

void resize_canvas(canvas_t& canvas, std::size_t width, std::size_t height)
{
    canvas.resize(height);
    for (auto& row : canvas) {
        row.resize(width);
    }
}

int main()
{
    canvas_t canvas {};
    resize_canvas(canvas, 320, 240);    // デフォルトの大きさで開始
    
    // ...

    // キャンバスの大きさを変更する。
    // 実際にはスクリプトを読み取って、width、height に新しい大きさを得る。
    std::size_t width = 300;
    std::size_t height = 300;
    resize_canvas(canvas, width, height);
}

std::vector<std::vector<Color>> の行と列のそれぞれの要素数を変更するので、まず行数を変更してから、各行の列数を変更しています。このように各行それぞれに resizeメンバ関数を呼べるので、行ごとに異なる要素数を持たせることも可能です(キャンバスとしてはおかしいですが)。

array.resize(3);
array[0].resize(5);
array[1].resize(10);
array[2].resize(7);

このような、要素数がきれいに揃っていない多次元配列をジャグ配列 (jagged array) と呼びます。ジャグ配列は、生の配列を普通に宣言する方法では実現できません。

【上級】動的なメモリ割り当てを利用すると、生の配列でもジャグ配列を作り出せます。

まとめ


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク


練習問題

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

生の二次元配列に、九九の結果を格納するプログラムを作成してください。

解答・解説

問題2 (基本★★)

std::vector を使って表現されたキャンバスの指定座標に、指定の色の点を描く関数を作成してください。

キャンバスの範囲外の座標を指定された場合には、単に無視するようにしてください。

解答・解説

問題3 (基本★★)

問題2の関数を利用して、キャンバスの指定の位置に、指定の大きさ・色の四角形を描くプログラムを作成してください。

解答・解説

問題4 (応用★★★)

5×5 の大きさの生の二次元配列に、1~75 から選ばれた重複のないランダムな数を、ランダムな位置に配置するプログラムを作成してください。

真ん中を特別あつかいしませんが、ビンゴゲーム3で配られるシートのイメージです。

解答・解説


解答・解説ページの先頭



更新履歴




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る