◎ 第九回:配列と繰り返しは仲良し! 〜真面目バージョン〜
● 配列とは
今まで使ってきた変数のイメージは箱を作り、その中に数字などのデータを格納する。
というものでした。
今回扱う『配列(Array)』では、いくつも並べた箱をイメージします。電車の
イメージが適当かもしれません。電車の各車両がそれぞれ箱(変数)に対応します。
そして、それぞれの箱(変数)のことを『(配列の)要素』と呼びます。
● 配列の特徴
配列の特徴は、“並んだ箱に番号が振ってある”ということです。「電車をイメージ
するのが適当だ。」といったのは このためです。電車には「○号車」というように
名前が付いていますよね。それと同様にそれぞれの箱に“1”“2”“3”…と名前が
付いています。
しかし、ここで注意する事が一つあります。配列の名前は“0から始まる”のです。
そのため、“一つ目が0”“二つ目が1”というように一つずれる事になります。
では、全部でn個の要素を持つ配列の、最後の要素の名前(番号)はいくつでしょう?
そうです。「 」ですね。
この名前は『(配列の)要素番号』や、『(配列の)添え字』と呼ばれます。
● なぜ配列か?
例えば、成績を管理することを考えましょう。もし、配列を使わなければ「seiseki1」
「seiseki2」「seiseki3」…というように、いくつもの変数を宣言し、それぞれに対して、
「seiseki1 = 79」「seiseki2 = 61」「seiseki3 = 89」というように同じような作業を
何度も行う必要があります。それを楽にしてしまおう。というのが配列です。
どのように便利かは、実際の例を見たほうが分かりやすいと思います。
● 配列の実装
int main(void)
{
int i; // カウンタ
int seiseki[9]; // 配列
for(i=0;i<9;i++)
{
seiseki[i] = 0; // 全ての成績を0で初期化
}
for(i=0; i<9; i++)
printf(“%d\n”, seiseki[i]); // 結果を表示
return 0; // main に 0 を返す
}
ここで、成績に関連する変数は「seiseki[9]」のみで、seiseki1, seiseki2
というようにダラダラとした宣言がありません。また、成績に関連する表示も
それぞれ一行ずつで全ての要素を操作しています。
● 具体的な説明
初期化の方法にはいくつかの方法があります。変数と違い配列はいくつもの要素
を持っているため「seiseki = 0」とは書けません。初期化の方法としては
@ 直接指定する記述
A 宣言時にのみ許される記述
の2種類があります。具体的に見てみましょう。
@ 直接指定する記述
配列の要素に割り当てられる名前(番号)を直接指定して値を代入します。
初期化以外の「値の参照」や「値の変更(代入)」も基本的にこの形に
なります。
int seiseki[9];
seiseki[0] = 10;
seiseki[1] = 20;
というように、宣言の時に要素数を指定した[]の中に、具体的な数字を
入れることで各要素を指定し、値を操作します。
これの応用として
for(i=0; i<9; i++)
seiseki[i] = 0;
や、
for(i=0; i<9; i++)
seiseki[i] = 10*(i+1);
などが考えられます。
ここで、注意が必要なのは[]の中に指定する数字です。最初にも言った
とおり、宣言の時に指定した数字と実際の要素数にはズレがあります。
ここでは要素数は“9”ですが、要素番号の最後は「8」です。(0から
始まっているため)
int seiseki[9];
seiseki[6] = 70; // 問題なし
seiseki[7] = 80 // 存在している
seiseki[8] = 90; // 配列の最終要素
seiseki[9] = 100; /* 存在していない! */
ここで、コンパイラが最後の「seiseki[9]=100;」をエラーとして
検出してくれれば良いのですが、コンパイラは文法に誤りがなければ
これをエラーとして検出しません。そのため、人間がきちんと管理する
必要があります。
なぜ ここまで しつこく言うかというと、後にも説明しますが配列は
メモリー操作に直接的に関わる部分であり、このようなエラーは
プログラムの暴走を いとも簡単に引き起こします。また、多くの場合
このようなエラーの検出は非常に困難になるからです。
A 宣言時にのみ許される記述
int seiseki[9] = {10, 20, 30, 40, 50, 60, 70, 80, 90};
と記述すれば、前から順に「seiseki[0]=10;」「seiseki[1]=20;」と
書いたのと同様の結果になります。
「{}」の最後に「;」を忘れないでください。
まず、ここで注意すべきは、これが宣言の時にしか許されない記述だと
いうことです。
/* これはOK */
int seiseki[9] = {10, 20, 30, 40, 50, 60, 70, 80, 90}; // 問題なし
/* これはダメ */
int seiseki[9];
seiseki = {10, 20, 30, 40, 50, 60, 70, 80, 90}; // こんな書き方はダメ!
と言うように、「宣言時」のみしか許されないということを覚えておいて
ください。
また、@とは逆でややこしいのですが、こっちの場合は初期化する値が
多すぎる場合は、コンパイラがエラーとして検出します。
int seiseki[9] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
// エラー!初期化子の数が多すぎます。
しかし、少ない場合は何も言いません。
int seiseki[9] = {10, 20}; // seiseki[2]以降の値は保証されません。
この場合、seiseki[2]〜seiseki[8]に関してはどんな値が入っているか
分かりません。もしかすると0かもしれませんが、-25839029かも
しれません。
初期化の@で示したように、値の操作は配列の要素番号を指定することで行います。
要素番号を指定した配列は、普通の変数と全く同じように扱えます。
int add = seiseki[1]+seiseki[2]; // 宣言時に計算して、結果を代入
ans = seiseki[3]*seiseki[4]; // かけ算(割り算・引き算もOK)
printf(“%d”, seiseki[5]); // printfに直接放り込むことも。
また、よく行われるのが for文との併用です。
for(i=0; i<9; i++)
printf(“%d”, seiseki[i]); // 要素番号iが順に変化して全てを出力
もう少し見やすいように
for(i=0; i<9; i++)
printf(“seiseki[%d]=%d”, i, seiseki[i]); // 要素番号も同時に表示
と言うのもよく使われます。
それから、配列同士の代入は間違いなくfor文で行われます。
for(i=0; i<9; i++)
seiseki1[i] = seiseki2[i]; // seiseki1配列にseiseki2配列の値を代入
● 配列とメモリー
配列を学ぶことはメモリーを勉強するのに、非常に良い機会です。配列はメモリー
そのものとすら言えるかもしれません。
プログラム中の全ての変数は、“メモリー上に領域を確保し、そこに値などを
格納する”ことで変数としての役割を果たします。基本的に変数はメモリー上の
どこの領域が確保され、使われるか分かりません。
メモリーには『番地(アドレス)』と呼ばれる位置を示す番号が与えられています。
メモリー自体が大きな配列になっていると考えると多少はイメージできるかも
しれません。
配列も変数同様メモリー上に確保されるのですが、このとき連なる要素は、
“全て連続して確保されます。”
|
配列でない場合 |
配列を用いる場合 | |||||
|
メモリ番地 203 204 205 206 207 208 209 210 211 |
変数名 |
値 |
|
メモリ番地 203 204 205 206 207 208 209 210 211 |
変数名 |
値 |
|
seiseki1 |
10 |
seiseki[0] |
10 | |||
|
|
|
seiseki[1] |
20 | |||
|
|
|
seiseki[2] |
30 | |||
|
seiseki3 |
30 |
seiseki[3] |
24863 | |||
|
|
|
seiseki[4] |
0 | |||
|
|
|
seiseki[5] |
-58345 | |||
|
|
|
seiseki[6] |
56846 | |||
|
seiseki2 |
20 |
seiseki[7] |
828 | |||
|
|
|
seiseki[8] |
-6868 | |||
上の例では、配列を使わずに3つの変数を確保し、初期化したものと、seiseki[9]
として配列を生成し、3つ目のseiseki[2]まで初期化したものです。このような形で
確保されるため、配列の添え字を変更するだけで、簡単に違う要素を扱えるのです。
ここで、それぞれの要素にアクセスするとき、矢印でその要素を指し示すイメージ
をもってください。例えば、seiseki[1]にアクセスするときは、「20」という値を
右から矢印が指し示すイメージです。そして、その次にseiseki[2]にアクセスする
ときはその矢印が一つ下に移動します。
このようなイメージを持っていると、次回の『ポインタ』についての理解が多少は
優しくなるかもしれません。
● 多次元配列
配列の応用として『多次元配列』というものがあります。その名の通り「次元」が
「多い」のです。
これまで見てきた例は seiseki[9]というように“[]”が一つしかついていません
でした。しかし、実は seiseki[9][40] というようにいくつも[]を連続して書くこと
ができます。
seiseki[9]のように“[]”が一つの配列を一次元配列といいます。[]が二つなら二次元
配列、三つなら三次元配列、四次元配列…。となります。とくに二次元以上の配列を
『多次元配列』といいます。なぜこのような事が必要なのでしょうか?
例えば、成績を管理するとします。添え字を学籍番号に対応させることで管理
できそうです。しかし、管理すべき科目は複数あります。今までの一次元配列では
一つの科目しか管理できません。そこで二次元配列を用います。二次元配列は
“九九の表”をイメージするといいでしょう。
|
seiseki[0][0] |
seiseki[0][1] |
seiseki[0][2] |
seiseki[0][3] |
seiseki[0][4] |
|
seiseki[1][0] |
seiseki[1][1] |
seiseki[1][2] |
seiseki[1][3] |
seiseki[1][4] |
|
seiseki[2][0] |
seiseki[2][1] |
seiseki[2][2] |
seiseki[2][3] |
seiseki[2][4] |
|
seiseki[3][0] |
seiseki[3][1] |
seiseki[3][2] |
seiseki[3][3] |
seiseki[3][4] |
|
seiseki[4][0] |
seiseki[4][1] |
seiseki[4][2] |
seiseki[4][3] |
seiseki[4][4] |
|
seiseki[5][0] |
seiseki[5][1] |
seiseki[5][2] |
seiseki[5][3] |
seiseki[5][4] |
|
seiseki[6][0] |
seiseki[6][1] |
seiseki[6][2] |
seiseki[6][3] |
seiseki[6][4] |
という感じになります。一般に「[行][列]」と書かれることが多いようです。
● 多次元配列の使用法
基本的には、一次元配列となんらかわりません。
同じように添え字で要素を指定して、変数と同様に代入したり、計算したりします。
注意すべきは、@宣言時の初期化とAfor文を使うときぐらいでしょう。
@ 宣言時の初期化
一次元配列のときに説明した宣言時の初期化は、次のようになります。
int seiseki[2][9] = { {0,1,2,3,4,5,6,7,8}, // 一行目の要素
{0,1,2,3,4,5,6,7,8} }; // 二行目の要素
各行の要素を{}で囲み、{}と{}の間に“,”(カンマ)を入れます。
これは、よく混乱するので、自分で簡単なテストプログラムを作って試すことを
お薦めします。
A for文を用いる初期化
これは、画面のx座標・y座標に対応して配列を設定した場合などに
よく用いられる方法です。
一般に、イメージが沸きやすいように添え字となるカウンタ変数に“x”“y”
という変数名を設定します。
#define X_MAX 256
#define Y_MAX 256
int x, y;
int luminance[Y_MAX][X_MAX];
for(x=0; x<X_MAX; x++)
for(y=0; y<Y_MAX; y++)
luminance[y][x] = 0; // luminance(輝度)を0で初期化
ここでは、直接数字を使わずに、#define を用いる方法で記述しました。
というか、これが基本です。突然256なんて数字が出てくるのは悪いプログラム
の見本です。そのような数字はマジックナンバー(魔法の数字→謎の数字)
と呼ばれます。そのプログラムを書いていない人には、なんのことやら
分からない数字になるからです。
いままでずっと使ってきた「seiseki[9]」の“9”もマジックナンバーであり、
悪いプログラムの見本ですね。
話が逸れてしまいましたので、戻しましょう。このように二次元配列は、
二重ループで処理します。同様に多次元配列は多重ループで処理することで、
処理が簡潔になります。
● ちょっと変わった配列の使い方
基本的に配列は数字を格納するために使われますが、少し変わった使い方をする事
があります。それが『フラグ』です。何らかの処理を行うべきかどうか?といった
ことを0か1で示します。if文において「if(1)」は成立。「if(0)」は不成立を意味する
ことから、“0”をNo。“1”をYesとすることが多いようです。
具体的には
bool is_rev[Y_MAX][X_MAX]; // reverseするかどうかのフラグ
というように、配列の中身としてbool(ブール)値(0か1の数)を格納することで
それをフラグとします。
これをこのまま
for(x=0; x<X_MAX;x++)
for(y=0; y<Y_MAX; y++)
if(is_rev[y][x])
{
… // 反転処理
}
というように使うわけです。
● 今日の課題
今日の課題は
「5×5の配列を宣言時の初期化で、0以外のランダムな数字に手動で初期化する。
その後、適当な場所に一つだけ0を手動で設定し、それを発見し、配列上での位置を
表示するプログラムを作成せよ」
です。
● プログラミング豆知識
プログラミング豆知識として、プログラミングにおける様々な豆知識を提供します。
今日は、@「include文」とA「main文」についての豆知識です。
@ include文
include文はご存じの通り、他のファイルからプログラムを読み込んで使う為の
ものです。よく使うのは<stdio.h>(standard input-output heder file:
エスティーディーヘッダなど、読み方は色々)や<math.h>などでしょうか。
include文は
#include <ファイル名>
もしくは
#include “ファイル名”
という構文で使われます。
この二つの使い分けは、説明が省略されていることが多いですが
「元々用意されているものは<>で。」「自分が作ったファイルは””で」という
暗黙の了解(?)があるようです。また、include文を使って、「〜.c」等の
ファイルを読み込ませる事もできますが御行儀が悪いので止めましょう。
A main文の引数と戻り値
引数と戻り値について説明すると、かなり長くなるので省略します。
簡単に説明すると引数(ひきすう)とは「関数名(x, y)」というように、関数名の
後ろの()の中に記述される値や、変数のことです。関数に値を与える時に
使います。
戻り値(/返り値ともいう)とは関数が返す値です。「double 関数名(int x,int y)」
となっていれば、「int型の引数2つ」をとり「double型の戻り値」を返す。
ということです。戻り値はreturn文を用い