配列 array

配列とは複数の変数をひとまとまりにして扱う方法である。

1次元配列

5つの整数を入力させ、入力された値を入力された順番と逆順で表示するプログラムreverse.cを作成せよ。(教科書p.97 例題6.1)

逆順で表示するためには、表示するまで値を覚えておかなければならない。つまり、入力された値を格納しておく変数が5つ必要になる。具体的なプログラム例は教科書p.98の例6.1のようになる。

この程度の簡単な例であれば頑張って書くことも可能であろう。しかし、値が100個とか沢山になるとお手上げである。そこで、複数の同じ型の変数を、同じ変数名を使って番号で区別する、配列が用意されている。

配列の宣言は例えば次のようになる:

int array[5];

この例ではarrayという名前で、5つ分のint型(整数型)の変数をまとめた配列を宣言している。配列が管理している変数のことを要素(element)と呼ぶ。配列の名前は変数と同じ命名ルールが適用される。すなわち、使用できるのはアルファベットと数字と_(アンダースコア)のみで、最初の文字に数字を使用することはできない。また予約語は使用することができない。

[ ]の中の値(配列の要素数、あるいは大きさと呼ぶ)は0以上の整数値の他、結果が0以上となる計算式でもよい。

配列の要素数には0が指定できてしまうが使える要素がないので、配列としては意味が無い。

配列の各要素は[ ]の中の整数値(要素番号、あるいは添字「そえじ」と呼ぶ)で使い分ける。つまり、上のようにして宣言した配列の各要素は、array[0]、array[1]、array[2]、array[3]、array[4]の5つとなる。要素番号は0から始まることに注意!!

配列は変数のアパートと考えるとよい。配列名がアパートの名前で、それぞれの要素に部屋番号が付いているというイメージである。

array[0]のように要素番号を指定することで普通の変数と同じように扱うことができる(教科書p.100 プログラム例6.2)。添字をつけずに、単にarrayのように配列名だけを指定すると正しく動作しないので注意!!

配列名だけを指定しても文法的には間違いではないのでコンパイルは通ってしまう。配列名と変数の関係についてはポインタの概念を理解する必要があるが高度な内容なので、ここでは配列名だけを指定しないこと、としておく。

ところが、具体的な要素番号をプログラム中で指定するのであればあまり意味が無い(同種のデータをまとめて扱えるという点では都合がよいが)。配列のおいしいのは、要素番号を変数や計算式で指定することができる点にある。例えば次のような書き方ができる:

int array[5];
int i;

i = 2;
array[i] = 4;  // 変数で要素番号を指定できる
array[i + 2] = 5;  // 計算式でもOK
printf("%d\n", array[array[i]]);  // arrayの要素自体も変数なので使うことができる

練習:このプログラムの断片を実行した時に、arrayの各要素の値はどうなっているか、最後のprintf文で表示される値は何か、考えてみよう。

この仕組みを使えば冒頭の例題は繰り返し構造を使って簡単に書ける。つまり、入力された値は配列の各要素に0番から順に格納しておき、表示するときは逆順に表示していけば良い。フローチャートで書くと次のようになる:

練習:フローチャートを元にreverse.cを作成せよ。

このように、配列は繰り返しと組み合わせて使われることが多い。for文の説明ではカウンタ変数を0から数え始めるようにしていたが、これは配列と組み合わせて使うためである。0から数え始めるということに慣れて欲しい。

配列の添字の範囲に関する注意

例えば

int array[5];

と宣言した配列の添字の範囲は0〜4である、と説明したが、実際にはこの範囲以外の要素番号を指定してもコンパイルエラーにはならないし、運が良ければそのまま実行できてしまう。例えば次のプログラムを見てみよう。

/*****
    outOfBounds.c
    配列の添字の範囲を超えてみる
    M.Minakuchi
*****/
#include <stdio.h>
#include <stdlib.h>

int main() {
    int array[10];
    int max;  // アクセスする要素番号の最大値
    int i;  // カウンタ変数

    printf("input max number: ");
    scanf("%d", &max);

    for (i = 0; i < max; i++) {
        array[i] = i;
        printf("%d\n", array[i]);
    }

    exit(0);
}

ダウンロードはこちら outOfBounds.c

練習:このプログラムの実行結果を予想し、入力値を変えて試してみよう(かなり大きな値にしないとそのまま実行できてしまうかもしれない)。

このようになってしまう理由は、C言語が配列をメモリ上でどのように表現しているかを知れば理解できる。ちょっと難しい話になるが雰囲気だけでも感じて欲しい。

C言語では配列を宣言した時に、指定された要素の個数分の変数に必要な量だけメモリを確保する。例えばint型の変数が4バイトで表現されるとすると、int array[5];として宣言された配列は4×5=20バイト分が確保される。配列arrayの各要素は、0番から順に割り当てられることになる。

実際には、配列変数arrayの値はこの確保されたメモリの先頭アドレスとなり、各要素はarrayの値と要素番号から計算して参照する。要素番号が負の値や最初に指定した要素数を超える値であっても、値の範囲チェックは行わずそのまま計算し、配列用に確保されたメモリでない部分のデータを読み書き(アクセス)してしまうことになる。この配列用でないメモリの部分に他の変数用に確保されたメモリがあると、その値を読み書きしてしまうことになる。あるいは、プログラムが使ってよい範囲外のメモリにアクセスしようとするとプログラムが異常終了してしまう。古いOSではOS自体が異常を起こすこともある。

配列で確保された範囲外の要素番号を使わないようにするのは不具合を起こさないプログラムを書く上で非常に重要である。様々なケースを想定して未然に防ぐように十分に注意して欲しい。

新しいプログラミング言語では要素番号が範囲を超えていないかどうかチェックする仕組みが入っているものもある。こういう点でもC言語は古い言語なのである。

練習問題

練習1. 奇数

次の処理を行うプログラムoddArray.cを作成せよ:
要素数20の整数型の配列を用意し、要素番号0から順に、1, 3, 5,...と1から始まる奇数を代入する。次に、この配列の要素の逆順に(値の大きい順に)表示する。

【実行例】
$ gcc -o oddArray oddArray.c
$ ./oddArray
39
37
35
(中略)
5
3
1
$

練習2. 奇数と偶数

次の処理を行うプログラムoddAndEven.cを作成せよ:
要素数20の整数型の配列を用意し、要素番号0から9までの要素には順に1, 3, 5,...と1から始まる奇数を代入、要素番号10から19までの要素には順に2, 4, 6,...と2から始まる偶数を代入する。次に、この配列の要素の逆順に(値の大きい順に)表示する。

【実行例】
$ gcc -o oddAndEven oddAndEven.c
$ ./oddAndEven
20
18
16
(中略)
4
2
19
17
(中略)
5
3
1
$

練習3. 漸化式

次の処理を行うプログラムrecurrenceArray.cを作成せよ:
要素数15の整数型の配列を用意し、要素番号0から順に, の漸化式の項を代入する。次に、この配列の要素の逆順に(値の大きい順に)表示する。

【実行例】
$ gcc -o recurrenceArray recurrenceArray.c
$ ./recurrenceArray
9565937
3188645
1062881
354293
118097
39365
13121
4373
1457
485
161
53
17
5
1
$

練習4. フィボナッチ数列

次の処理を行うプログラムfibonacchi.cを作成せよ:
要素数20の整数型の配列を用意し、要素番号0から順に, , の漸化式(フィボナッチ数列)の項を代入する。次に、この配列の要素を順に表示する。

【実行例】
$ gcc -o fibonacchi fibonacchi.c
$ ./fibonacchi
1
1
2
3
5
8
(中略)
2584
4181
6765
$

練習5. 最大値

次の処理を行うプログラムmax.cを作成せよ:
要素数100の整数型の配列を用意する。最初にデータの個数を入力させ、次にこのデータの個数だけ整数値を入力させる。そして、入力されたデータの中の最大値を表示する。入力値のエラーチェックは省略してよい。

【実行例(下線部は入力値、入出力メッセージはこの通りでなくてもよい)】
$ gcc -o max max.c
$ ./max
input number of data: 5
input No.0 data: 11
input No.1 data: 43
input No.2 data: 54
input No.3 data: 3
input No.4 data: 42
max value = 54
$

2次元配列

同種のデータの単なる集まりや数列のようなデータは1次元配列で表現できる。一方、表計算ソフトのように2次元、すなわち縦と横の2つの軸のあるデータを表現するには2次元配列を使うと良い。

2次元配列の宣言は、例えば次のようになる:

int array[3][5];

1次元配列と異なっているのは[ ]が2つ書かれている点である。それぞれ、行番号と列番号と考えればよい。模式的に表すと教科書p.107図6.5のようになる。

使い方は1次元配列と同様に、2つの添字を指定することで要素を指定する。1次元配列と同様にそれぞれの添字は0から始まることに注意。有効な範囲外の要素番号を指定した場合の結果は不定で、最悪プログラムが異常終了するのも1次元配列と同じ。

練習:上のように3x5の2次元配列として宣言された配列arrayに対して、正しくない(プログラムが異常を起こす可能性のある)書き方を次の中から選べ。
array[0][4] = 3;
array[0][5] = 4;
array[1][3] = array[2][0];
array[3][1] = array[1][3] + 1;
array = 0;
array[2] = 123;
array[0] = array[1];

例題:9x9の2次元配列が単位行列となるように値を初期化し、表示するプログラムidentity.cを作成せよ。初期化および表示には繰り返し処理を使うこと。また、単に表示するだけでなく2次元配列に値を格納すること。なお、単位行列とは対角要素は1、それ以外の要素は0である正方行列のことである(下の実行例を参照)。

【実行例】
$ gcc -o identity identity.c
$ ./identity
1 0 0 0 0 0 0 0 0 
0 1 0 0 0 0 0 0 0 
0 0 1 0 0 0 0 0 0 
0 0 0 1 0 0 0 0 0 
0 0 0 0 1 0 0 0 0 
0 0 0 0 0 1 0 0 0 
0 0 0 0 0 0 1 0 0 
0 0 0 0 0 0 0 1 0 
0 0 0 0 0 0 0 0 1 
$

2次元の配列の要素全体を処理するには、まず0行目について0列目、1列目、と順に処理する。最後の列に達したら次の行に進み、0列目から順に処理する。これを最後の行まで繰り返す、という手順になる。この手順を実現するには2重ループを使えばよい。

対角要素とは行番号と列番号が同じ要素であるので、2重ループの2つのカウンタ変数(それぞれ行番号と列番号に相当)が一致すればその要素の値を1、一致しなければ0とすればよい。

この処理をフローチャートで描くと次のようになる。

多次元配列

3次元以上の配列も同様に宣言して使うことができる。3次元配列の宣言は例えば次のようになる:

int array[3][5][2];

4次元以上になると意味が直感的に理解しにくく、必要となる場合も限られてくるが、原理的には何次元でも使用できる。ただし、要素数が爆発的に増加するので必要になるメモリ量には注意する必要がある。

練習問題

練習6. 2次元配列計算

次の処理を行うプログラムmatrixCalc.cを作成せよ:
まず3x3の配列を宣言し、[0][1]、[1][0]、[1][2]、[2][1]の要素の値を順に入力させる。残りの要素は上下左右の要素の和となるよう計算する。最後に、配列の値を表示する。値と値の間はタブ(\t)を使って表示すること。
なお、入力および計算は繰り返し処理を使う必要はない。

【実行例】
$ gcc -o matrixCalc matrixCalc.c
$ ./matrixCalc
input value for [0][1]: 1
input value for [1][0]: 2
input value for [1][2]: 3
input value for [2][1]: 4
3 1 4
2 10 3
6 4 7 $

練習7. 九九

九九の表を2次元行列で作成し、次の実行例のように表示するプログラムkuku.cを作成せよ。積が奇数となる値には*を付け、値と値の間はタブを使って表示すること。

【実行例】
$ gcc -o kuku kuku.c
$ ./kuku
*1      2       *3      4       *5      6       *7      8       *9
2 4 6 8 10 12 14 16 18
*3 6 *9 12 *15 18 *21 24 *27
4 8 12 16 20 24 28 32 36
*5 10 *15 20 *25 30 *35 40 *45
6 12 18 24 30 36 42 48 54
*7 14 *21 28 *35 42 *49 56 *63
8 16 24 32 40 48 56 64 72
*9 18 *27 36 *45 54 *63 72 *81 $

課題1.

次の処理を行うプログラムaverage.cを作成せよ:
要素数100の整数型の配列を用意する。最初にデータの個数を入力させ、次にこのデータの個数だけ整数値を入力させる。入力されたデータの平均値を計算し、平均値より大きい値のデータと要素番号を表示する。入力値のエラーチェックは省略してよい。

【実行例(下線部は入力値、入出力メッセージはこの通りでなくてもよい)】
$ gcc -o average average.c
$ ./average
input number of data: 5
input No.0 data: 11
input No.1 data: 43
input No.2 data: 54
input No.3 data: 3
input No.4 data: 42
average = 30
No.1: 43
No.2: 54
No.4: 42
$

提出期限:6月30日(月) 13:15

課題2.

次の処理を行うプログラムdivideTwo.cをHandyGraphicを使用して作成せよ:
5x5の2次元配列を用意する。まず、[0][0]〜[0][4]の要素に入力された値を格納する。[1][0]〜[1][4]は[0][0]〜[0][4]の値を2で割った値とする。同様に、[2][0]〜[2][4]は[1][0]〜[1][4]の値を2で割った値とし、……を[4][0]〜[4][4]まで繰り返す。
次に200x200のウィンドウを開き、次の図の配置で配列の要素が偶数なら青、奇数なら赤の四角で塗りつぶす。

(入力値が順に1, 2, 4, 8, 16の例)

(入力値が順に12, 13, 14, 15, 16の例)

(6/12、2つめの実行例を修正しました。縦一列分が1つの入力値に相当します。実行結果は何を表しているか考えてみてください。)

(6/30、2つめの実行例を修正したつもりが、図を更新し忘れてたようです。ごめんなさい。新しいのに置き換えました。縦と横が入れ替わった形になっています。どちらでも正解とします。既に提出した人は差し替え不要です。)

ヒント:配列要素をa[i][j]とすると、i >= 1の要素については、a[i][j] = a[i - 1][j] / 2; で計算される。

提出期限:7月3日(木) 24:00