第六回-01 配列の基礎


配列の宣言と利用

配列とは複数のデータを格納するためのデータ構造である。 以下のプログラムは、整数を5つ格納できる配列 a を定義し、利用するプログラムである。

必要に応じてあらかじめ iostream を include しておくこと。

#include <iostream>


int a[5];   // 整数を格納できる要素数 5 の配列の宣言

a[0] = 10;    // 配列へのデータの書き込み
a[1] = 2;
a[2] = 0;
a[3] = -5;
a[4] = -23;

// 配列のデータをコンソールに表示
for(int i=0 ; i<5 ; i++){
	std::cout << "配列aの" << i << "番目の値は" << a[i] << "\n";
}

実行したときの出力がこちら。

配列aの0番目の値は10
配列aの1番目の値は2
配列aの2番目の値は0
配列aの3番目の値は-5
配列aの4番目の値は-23

さて、プログラム中で「int a[5];」の部分に着目して欲しい。
これは、整数を5つ格納できる配列 a を定義する命令である。

変数について学んだ時と同じ考え方をすれば、これは「整数を5つ格納できる棚」を用意したことに相当する。



図に示されているように、「int a[5];」という宣言で、a[0]、a[1]、a[2]、a[3]、a[4] という名前の箱 (変数) が使えるようになる、と考えればよい。

このように、「宣言時は 5 とし、使うときは 0 ~ 4 という添字を使う」のが配列を使う際につまづきやすいところである。
これは繰り返し使って慣れるしかない。

[注意]
1 年生で学んだ Visual Basic における配列では、
Dim a(5) As Integer
により、a(0)、a(1)、a(2)、a(3)、a(4)、a(5) の 6 つの変数が使えたことに注意。
ただし、プログラミング言語の世界では、むしろVisual Basic のように 6 つの変数が使えることの方が特殊である。

C/C++、Java、C# などのように「宣言『int a[5];』により a[0] ~ a[4] の 5 つの変数が使える」という形式に早く慣れよう。

上のプログラム例では、配列の 5 つの要素 a[0] ~ a[4] に値を格納する際、以下のように変数と同様に代入文を書いていた。

a[0] = 10;    // 配列へのデータの書き込み
a[1] = 2;
a[2] = 0;
a[3] = -5;
a[4] = -23;

この5つの命令により、以下のような状況が生まれる。
変数に慣れている学生にはそれほど違和感はないであろう。



また、上のプログラム例では、配列のそれぞれの要素に格納された値をコンソールに表示する際、
以下のように for 文を使っていたことに注意して欲しい。

// 配列のデータをコンソールに表示
for(int i=0 ; i<5 ; i++){
	std::cout << "配列aの" << i << "番目の値は" << a[i] << "\n";
}

「for(int i=0 ; i<5 ; i++)」により、「i が 0 から 4 まで変化する」という for 文となり、
a[i] をコンソールに表示することで a[0] ~ a[4] の全てが表示されるのである。

なお、「int a[5];」による宣言では a[5] は使ってはいけない、ということは以下で確認できる。
すなわち、「a[5] = -1;」という a[5] への代入文 (太字で表示した) を一文挿入して実行を試してみるのである。

int a[5];   // 要素数 5 の配列の宣言

a[0] = 10;    // 配列へのデータの書き込み
a[1] = 2;
a[2] = 0;
a[3] = -5;
a[4] = -23;
a[5] = -1;

// 配列のデータをコンソールに表示
for(int i=0 ; i<5 ; i++){
	std::cout << "配列aの" << i << "番目の値は" << a[i] << "\n";
}



配列の初期化

変数には、宣言と同時に値を与えておく「初期化」があった。
配列にも同様に初期化がある。以下のように、コンマ (,) で区切ったデータを {} で括って配列に渡せば良い。

int a[5] = {10, 2, 0, -5, -23};   // 要素数 5 の配列の初期化

// 配列のデータをコンソールに表示
for(int i=0 ; i<5 ; i++){
	std::cout << "配列aの" << i << "番目の値は" << a[i] << "\n";
}

これにより、下図のような状況となる。



なお、配列の初期化にはいくつかのバリエーションがある。最初から覚える必要はないが、いくつか書き出しておこう。

まず、初期化の場合、左辺の要素数を省略して書くことができる (つまり、a[5] ではなく a[])。

int a[] = {10, 2, 0, -5, -23};   // 要素数 5 の配列の初期化

また、左辺の要素数に対して、右辺の数値の個数が少なかった場合、その要素は 0 で初期化される。

int a[5] = {10, 2, 0, -5};   // 値の与えられなかった a[4] は 0 で初期化される。

また、左辺の要素数よりも多いデータを右辺に与えると、エラーになる。

int a[5] = {10, 2, 0, -5, -23, -7};   // エラー



配列とfor文

さて、先程の例で、配列に格納されているデータをコンソールに表示するために for 文が使われていたことを思い出そう。
配列は、a[i] という形式で書くことで値を格納したり取り出したりできるのであるから、i をカウンタとして使った for 文と相性がよい。

さらに言えば、配列を使いこなすには for 文の理解は必須と言える。
そのような例を見てみよう。

ここで、要素数10の配列に 0以上の3の倍数を小さい方から順に格納する、という例を考える。
一番原始的な方法は下記の通り。

int a[10];  // 整数を格納できる要素数 10 の配列の宣言

// 3の倍数を10個格納
a[0] = 0;
a[1] = 3;
a[2] = 6;
a[3] = 9;
a[4] = 12;
a[5] = 15;
a[6] = 18;
a[7] = 21;
a[8] = 24;
a[9] = 27;

// 要素数 10 の配列のデータをコンソールに表示
for(int i=0 ; i<10 ; i++){
	std::cout << "配列aの" << i << "番目の値は" << a[i] << "\n";
}

見ての通り、3の倍数を列挙して順に格納しているだけである。

しかし、3の倍数のようにルールが明確な数を、上記の様にに列挙して代入するというプログラマはまずいない (いてもクビになるだろう)。
そういう意味では、以下のように配列の初期化を用いるのも駄目な例である。

// 3の倍数を10個格納
int a[10] = {0, 3, 6, 9, 12, 15, 18, 21, 24, 27};

なぜこれらがダメかというと、配列の要素数 (ここでは 10) が 100 や 1000 などと大きくなったときに
すぐに対応できなくなるためである。

このように、配列に格納するデータのルールが決まっている場合は for 文を用いてデータを格納すれば良い。

int a[10];   // 整数を格納できる要素数 10 の配列の宣言

// 3の倍数を10個格納
for(int i=0 ; i<10 ; i++){
	a[i] = 3*i;
}

// 要素数 10 の配列のデータをコンソールに表示
for(int i=0 ; i<10 ; i++){
	std::cout << "配列aの" << i << "番目の値は" << a[i] << "\n";
}

for 文は i=0 で始まり i<10 のあいだ繰り返される。すなわち i が 0~9 の間、 a[i] = 3*i; という命令が繰り返される。
それにより、a[0]~a[9] に3の倍数が格納されることがわかるだろう。

それに対し、「i=1 で始まり i<=10 のあいだ繰り返される for文」、
すなわち i が 1~10 の間繰り返される for 文ではここでは問題があることも理解して欲しい。
もちろん、使ってはならない a[10] を使うことになるからである。

このように for 文を正しく用いることで、配列の要素数が 100 や 1000 と増えても問題なく対応できるプログラムとなる。


[演習] 配列で数学の数列を実現してみよう

ルールの明確なデータを配列に格納する際、for 文を用いると良いということを学んだ。
配列と for 文の組み合わせに慣れるため、そのような練習をもう一つ行ってみよう。

[演習]
整数を格納できる要素数10の配列 a を宣言する。
配列 a の i 番目の要素 a[i] に、以下の漸化式を満たす数列 ai の値を格納せよ。
ai = 2 ai-1 - 1    (i>=1)
ただし、a0 = 0 とする。

この問題を解くためには、高校数学で学ぶ漸化式について皆さんが正しく理解している必要がある。
といっても、問題文に必要なことは全て書いてあるので、
皆さんが注意深く式や文章を読むことができるかどうかがポイントである。

さて、この節の冒頭で「ルールの明確なデータを配列に格納する」と述べた。
漸化式「ai = 2 ai-1 - 1    (i>=1) 」がこの「ルール」に相当するのはすぐに想像がつくであろう。

それでは、漸化式についている条件「i>=1 (i は 1 以上)」はどういう意味だろうか?
もちろん、漸化式「ai = 2 ai-1 - 1」を i=0 の場合に適用してはいけない、という意味である。

では、i=0 の場合、どのように数列の値 a0 や配列の値 a[0] を扱えば良いのだろうか?
もちろん、問題文にある数列の初期値 a0 = 0 を用いるのである。
すなわち、i=0 の場合は数列でも配列でも特別扱いをしなければならない、ということである。

以上を踏まえると、必要なプログラムは以下のようになる。

int a[10];

// 数列の初期値
a[0] = 0;

// 漸化式は i>=1 のときのみ
for(int i=1 ; i<10 ; i++){
	a[i] = 2*a[i-1]-1;
}

// コンソールへの表示は i=0 からで OK
for(int i=0 ; i<10 ; i++){
	std::cout << "配列aの" << i << "番目の値は" << a[i] << "\n";
}

プログラムを良く見ると、漸化式「a[i] = 2*a[i-1]-1;」を計算しているのは「for(int i=1 ; i<10 ; i++)」においてである。
すなわち i が 1 から 9 まで、ということである。

i=0 の場合は、「a[0] = 0;」として特別扱いしていることがわかる。


配列の実用例

実は配列は画像処理の例でこれまで何度も登場している。思い出してみよう。
以下はこれまで何度も登場している、画像処理の本質部分である。



このうち、outImage[ ... ] (いつも右側に表示される出力画像)、inImage[ ... ] (いつも左側に表示される入力画像) が実は配列である。
かぎかっこ ([]) の内部で添字を指定するのだが、画像の (i,j) の位置の要素がうまく指定されるよう
関数という機能を用いているが、それはまだ先の話。

このとき、画像ファイル (jpg) と配列とそれを計算する CPU との間の関係を示す イメージ図は以下のようになる。



画像データをプログラムから利用するためには、一度それをメインメモリに読み込まねばならない。
それは、現在のコンピュータがそのような仕組みになっているからである。
データとプログラム本体をメモリに置いて実行するこの方式のコンピュータをフォンノイマン型コンピュータと言う。

また、こうすることにより多くのメリットがある。 今、ハードディスクとメインメモリには、
下の表にあるように「メインメモリの方が容量が小さいが高速」という関係がある。

記憶装置 転送速度 記憶容量
ハードディスク 100 MByte/s 前後 数百 GByte 程度
メインメモリ 数 GByte/s (ハードディスクより一桁速い) 数 GByte 程度 (ハードディスクより二桁小さい)
二次キャッシュ (L2 Cache) 最高 100 GByte/s 程度 (メインメモリより二桁速い) 数百 KByte 程度 (メインメモリより四桁小さい)
一次キャッシュ (L1 Cache) 二次キャッシュよりさらに高速 二次キャッシュよりさらに小さい

この時、画像処理のプログラムは画像データである inImage[…] と outImage[…] に 何度もアクセスする。
このように何度もアクセスするデータは高速にアクセスできるメインメモリに 配置してプログラムを実行した方が効率が良いわけである。

プログラミングについて詳しく学びたいと考えている学生は、 メモリ上のデータの配置がどうなっているかなども意識するようにすると勉強になる。
(後期のプログラミング演習 II ではそういう話をできれば良いと思う)

なお、CPU の内部には「一次キャッシュ」と「二次キャッシュ」と呼ばれ、 メモリよりも高速だが容量は小さい記憶領域が存在する。
このような記憶領域の構成をメモリ階層という。


配列の動的確保

配列を使い出すと、配列のサイズをプログラムの実行中に決定したくなる機会に遭遇することが多い。

そのような際、気持ちとして以下のような宣言を書きたくなる。
つまり、変数 n に作りたい配列のサイズが格納されているときに、「double arr[n];」で配列を宣言したいわけである。

int n = 3;
double arr[n];                           // 要素数 n の配列の宣言→コンパイルエラー!

しかし、実際に試してみればわかるが、これはエラーが出てコンパイルできない。
実は、配列のサイズはコンパイル時に確定していなければならないのである。

画像処理の例で言えば、これは「画像を格納する配列のサイズ (画像のサイズ) はコンパイル時に決まっていなければならない」
ということを意味する。

しかし、一般的な画像処理アプリケーションは、どのようなサイズの画像を読み込むかはコンパイル時には決まっていないことがほとんどであり、
コンパイル時に画像サイズを決めておかなければいけないというのは大変不便である。

実際には、プログラムを起動してからサイズを決定するためには「ポインタ」を用いる必要がある。
ポインタの詳細は後期のプログラミング演習 II で学ぶ予定であるが、以下では利用例のみを紹介する。

int n = 3;
double *arr;

arr = new double[n];     // 要素数 n の配列として使える

// arr[0], arr[1], …, arr[n-1] を使った何らかの処理

delete[] arr;         // 使い終ったら削除の処理が必要

ここでは配列のメモリ領域が実行時に確保されており、このことを 配列の (メモリの) 動的確保という。


多次元配列

複数の添字で要素を指定する配列を多次元配列という。二次元配列ならば a[i][j] などである。
これまで学んだのは一次元配列である。

一次元配列 a[i] は数学で学ぶ数列 ai を思い浮かべると良かったが、 二次元配列 a[i][j] は数学で学ぶ行列 aij に対応すると考えれば良いだろう。
使用例は以下の通りである。

double arr[2][3];    // 要素数 2×3 の二次元配列の宣言

// 使えるのは
// arr[0][0],  arr[0][1],  arr[0][2]
// arr[1][0],  arr[1][1],  arr[1][2]
// の6個

for(int i=0 ; i<2 ; i++){
   for(int j=0 ; j<3 ; j++){
      // a[i][j]に関する操作
  }
}


また、二次元配列を動的に確保する方法は以下の通り。

int m, n; 
m = 2;
n = 3;
double **arr;    // ポインタへのポインタ

arr = new double*[m];

for(int i=0 ; i<m ; i++){
	arr[i] = new double[n];
}

// 以上で arr[m][n] として使える

// 使い終ったらメモリを解放
for(int i=0 ; i<m ; i++){
	delete[] arr[i];
}

delete[] arr;






←第五回課題第六回-02 配列の実用例→

非情報系学生のための C/C++ 入門に戻る