配列 array (教科書7.1〜7.6節)

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

1次元配列

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

逆順で表示するためには、表示するまで値を覚えておかなければならない。つまり、入力された値を格納しておく変数が5つ必要になる。これまでに習った範囲でこのプログラムを作ると、例えば次のようになる:

/*****
      reverse.c
      5つの整数を入力させ逆順に表示する
      M.Minakuchi
*****/
#include <stdio.h>

int main() {
  int number0, number1, number2, number3, number4;

  printf("input No.0: ");
  scanf("%d", &number0);
  printf("input No.1: ");
  scanf("%d", &number1);
  printf("input No.2: ");
  scanf("%d", &number2);
  printf("input No.3: ");
  scanf("%d", &number3);
  printf("input No.4: ");
  scanf("%d", &number4);

  printf("No.4 = %d\n", number4);
  printf("No.3 = %d\n", number3);
  printf("No.2 = %d\n", number2);
  printf("No.1 = %d\n", number1);
  printf("No.0 = %d\n", number0);

  return 0;
}

この程度の簡単な例であれば頑張って書くことも可能であろう。しかし、値が100個とか沢山になるとお手上げである。

そこで、複数の同じ型の変数を、同じ変数名を使って番号で区別する、配列が用意されている。

配列の宣言(教科書7.2節)

配列の宣言は次の形式である:

配列のデータ型 配列名[要素数];

変数の宣言に[要素数]が追加されている。[]はカギ括弧。要素数はこの配列が持っている変数の個数で、整数値を指定する。

具体的には例えば次のように書く:

int array[5];

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

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

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

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

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

配列の利用(教科書7.3節)

array[0]のように要素番号を指定することで普通の変数と同じように扱うことができる(教科書p.184の代入の例)。

上記のプログラムは配列を使って次のように書き直せる:

/*****
      reverse.c
      5つの整数を入力させ逆順に表示する
      配列を使ったバージョン
      M.Minakuchi
*****/
#include <stdio.h>

int main() {
  int number[5];

  printf("input No.0: ");
  scanf("%d", &number[0]);
  printf("input No.1: ");
  scanf("%d", &number[1]);
  printf("input No.2: ");
  scanf("%d", &number[2]);
  printf("input No.3: ");
  scanf("%d", &number[3]);
  printf("input No.4: ");
  scanf("%d", &number[4]);

  printf("No.4 = %d\n", number[4]);
  printf("No.3 = %d\n", number[3]);
  printf("No.2 = %d\n", number[2]);
  printf("No.1 = %d\n", number[1]);
  printf("No.0 = %d\n", number[0]);

  return 0;
}

単にnumberと、配列名だけを指定すると正しく動作しないので注意!!現時点では、配列は必ず[要素番号]を付けて使う、と覚えておこう。

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

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

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から数え始めるということに慣れて欲しい。

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

使用してよい要素番号は0〜要素数-1の範囲と説明した。例えば、

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言語は古い言語なのである。

配列の初期化(教科書7.4節)

配列の各要素の値は、配列を宣言しただけでは値が不定、すなわち、どのような値になっているか決まっていない。これは変数と同様である。例えば次のプログラムは、要素数10の配列を作って値を表示するが、実行させると値が初期化されていないことが確認できる(運良く0ばかり表示されるかもしれない)。

/*****
      inittest.c
      配列の初期化のテスト
      M.Minakuchi
 *****/
#include <stdio.h>

int main() {
  int array[10];
  int i;

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

  return 0;
}

変数では宣言時に値を代入することで初期化することができた。同様に、配列も宣言時に値を代入することができる。配列では、複数の値をまとめて扱うので、次のような書式が用意されている。

配列のデータ型 配列名[要素数] = {値0, 値1, 値2, ...};
...の部分は必要な個数だけ値を列挙する。

右辺の{}で囲まれた部分を初期化子initializerと呼ぶ。具体的には次のような書き方になる(教科書p.190)

int test[5] = {80, 60, 22, 50, 75};

初期化子の個数が配列の要素数より少ない場合、要素番号0から順に代入され、余った要素には0が代入される。例えば次の例では、要素番号3と4の要素の値は0となる。

int test[5] = {80, 60, 22};

これを使えば、配列のすべての要素を0で初期化したい場合、次のように書けばよい:

int test[5] = {};

逆に、初期化子の方が多い場合はコンパイル時に警告が出るが、コンパイル・実行はできる。

何に配列を使うか

複数の同種類の数値を扱う場合は配列を使うことを考えるとよい。例えば10人分の身長データを扱う場合など。

複数の数値であっても意味が異なる場合は1つの配列にまとめない方がよい。例えば10人分の身長と体重データを1つの1次元配列でまとめて扱おうとすると、どの要素が身長でどの要素が体重か区別しなければならなくなる。2つの1次元配列に分けるか、2次元配列を使う方がよい。

※ 実行速度等の理由でそういう使い方をすることも無きにしも非ずだが、もうそういう時代でもないだろう。人間が間違わないようにプログラムを書くことを最優先にしてよい。

確認課題

以下、出来たら教員に確認を受けること。原則的に易しい順となっているが、順序どおりに取り組む必要はない。

確認課題1. 身長

身長が163cm、157cm、176cm、180cm、166cmの5人がいて、0〜4までの番号を付けているとする。この5人の身長の値を配列に格納し(上述の初期化子を使えばよい)、入力した番号の身長を表示するプログラムheight.cを作成せよ。なお、入力値の正当性チェックは省略してよい。

【実行例。下線部は入力値の例。】
% gcc -o height height.c
% ./height
番号? 0
0番の身長は163cmです。
% ./height
番号? 3
3番の身長は180cmです。
%

ヒント:要素数5の配列を宣言し、指定された値で初期化する。次に、整数値を入力させ、その値で配列の要素を参照する。

確認課題2. 身長・改

確認課題1.で作成したプログラムに入力値の正当性チェックを追加し、配列の要素番号の範囲外の値が入力された場合は番号を再入力させよ。(ソースファイル名はheight.cのままでもよいし、height2.cとしてもよい。)

【実行例。下線部は入力値の例。】
% gcc -o height height.c
% ./height
番号? 0
0番の身長は163cmです。
% ./height
番号? -1
番号? 5
番号? 3
3番の身長は180cmです。
%

ヒント:入力値が配列の添字の有効範囲になるまで入力を繰り返す。

確認課題3. 身長・改二

確認課題2.のプログラムにおいて、このクラスの学生番号は54400から始まる番号であるとする。つまり、この5人の学生番号は54400、54401、54402、54403、54404である。この時、入力値を学生番号とするように、確認課題2.のプログラムを修正せよ。(ソースファイル名はheight.cのままでもよいし、height3.cとしてもよい。)

【実行例。下線部は入力値の例。】
% gcc -o height height.c
% ./height
学生番号? 54400
54400番の身長は163cmです。
% ./height
番号? 50000
番号? 54405
番号? 54403
54403番の身長は180cmです。
%

ヒント:学生番号を配列の要素番号に変換する。

確認課題4. 漸化式

次の処理を行うプログラム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
%

ヒント:要素番号0の値は直接代入する。1〜14は、漸化式に従って順に繰り返しで計算する。1つ前の要素の値を使って計算するのがポイント。各項の計算と表示を一つの繰り返しでやろうとしないこと。計算する繰り返しと、表示する繰り返しを分ければよい。

確認課題5. フィボナッチ数列

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

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

ヒント:確認課題4と同様、要素番号0と1の値は直接代入し、残りを漸化式に従って順に繰り返しで計算する。

確認課題6. 合否判定

10人の試験の点数が66, 98, 56, 48, 73, 35, 87, 60, 52, 65であったとする。合格点を入力すると合格点以上となる人数を表示するプログラムpassCheck.cを作成せよ。点数データは配列の初期化子を使ってよい。例えば、
int score[10] = {66, 98, 56, 48, 73, 35, 87, 60, 52, 65};

【実行例(下線部は入力値)】
% gcc -o passCheck passCheck.c
% ./passCheck
合格点: 60
合格者は6人
% ./passCheck
合格点: 50
合格者は8人
% ./passCheck
合格点: 70
合格者は3人
%

確認課題7. 番号検索 [advanced]

10人のIDと鍵の番号が下の表のようになっているとする。IDを入力すると、そのIDの人の鍵の番号を表示するプログラムfindKey.cを作成せよ。なお、入力したIDの人がいない場合は、いないというメッセージを表示せよ。必ず繰り返しを使うこと。IDと鍵のデータは配列の初期化子を使ってよい。例えば、
int id[10] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
int key[10] = {114, 194, 223, 315, 326, 404, 514, 634, 777, 819};

ID  11  13  17  19  23  29 
鍵の番号  114  194  223  315  326  404  514  634  777  819 

【実行例(下線部は入力値)】
% gcc -o findKey findKey.c
% ./findKey
IDを入力してください: 2
ID2の鍵の番号は114です
% ./findKey
IDを入力してください: 19
ID19の鍵の番号は634です
% ./findKey
IDを入力してください: 30
IDが見つかりません
%

ヒント:まずIDを入力させる。次に、繰り返しを使ってIDが一致する要素番号を見つけ、見つかったらその要素番号で鍵を参照し表示してプログラムを終了する(プログラムの途中でreturn 0;を呼べば即座に終了することができる)。配列の最後まで繰り返してもIDが見つからない場合は「見つかりません」と表示する。

本日の提出課題

提出課題1. 平均値

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

提出期限:次の土曜日の24:00まで

【実行例(下線部は入力値、入出力メッセージはこの通りでなくてもよい)】
% 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
%

ヒント:データの入力、平均値の計算、平均値より大きいデータの表示は1つの繰り返しで書くのは無理がある。それぞれ別の繰り返しに分けるのがよい。

なお、このプログラムも提出課題2と同様にして入力リダイレクションを使うことができる。

提出課題2. 最大値

次の処理を行うプログラムmax.cを作成せよ:
まず要素数100の整数型の配列を用意する。次にデータの個数を入力させる。そして、このデータの個数だけ整数値を入力させる。最後に、入力されたデータの中から最大値を探して表示する。

入力値のエラーチェックは省略してよい。また、入力データは必ず配列に記憶させること(配列を使わないプログラムは不可)。

提出期限:次回授業開始時まで

【実行例(下線部は入力値、入力メッセージは下記の理由で省略。出力メッセージはこの通りでなくてもよい)】
% gcc -o max max.c
% ./max
5
11
43
54
3
42
max value = 54
%

上の実行例中、最初の5はデータの個数が5個であることを入力しており、残りの11, 43, 54, 3, 42が5個分のデータである。

なお、UNIXコマンドの応用の回で紹介した入力リダイレクションを使えば、手で値を入力する代わりに、入力値をテキストファイルに書いておいて、プログラムに入力値として与えることができる。例えば、maxdata1.txtのように、1行ずつ入力データが書いてあるファイルを用意して、次のように実行することができる:

【実行例】
% gcc -o max max.c
% ./max < maxdata1.txt
max value = 54
%

入力を促すメッセージを表示すると、リダイレクションで入力した値は表示されずに少し変な感じになるので省略している。

このプログラムの実行テスト用に次の3つのデータを使うとよい。そのままクリックするとブラウザ上で開いてしまうので、リンクを右クリック(あるいはControlを押しながらクリック)して、「リンク先のファイルを保存」を選択する。保存したファイルは「ダウンロード」フォルダ(ターミナル上では ~/Downloads/ ディレクトリ)、あるいは保存時に指定したフォルダ(ディレクトリ)に置かれるので、プログラムを作成している作業用ディレクトリに保存したデータファイルを移動させるとよい。
maxdata1.txt
maxdata2.txt
maxdata3.txt

リダイレクトでデータを入力するプログラム例はこちら redirectinput.c

ヒント:最大値を探すには、まず最初の要素の値を仮の最大値とする。そして、この仮の最大値と次の要素を比較して、仮の最大値の方が小さい場合は、比較した要素の値を次の仮の最大値とする。繰り返しですべての要素と比較し終わった時、仮の最大値が本当の最大値である。