配列(1)

目次

配列とは

コンピュータによる情報処理においては,四則演算や比較などの「データ処理法」と,条件分岐・繰り返し・関数などの制御構文にくわえて, コンピュータ上で「データをどのように表現するか」の設計が重要となる.
これらは,それぞれ「アルゴリズム」と「データ構造」と呼ばれ,よいプログラムを書くためには双方を熟知している必要がある.

工学で用いる実用的なプログラムでは,数MB〜数GB以上の大量のデータを扱うことが多い.
例えば,高速度カメラによる動画像処理では,1000x1000pixel, 1000fps(毎秒1000枚の画像取得)のデータ取得があり,これは1秒あたり1GBのデータを扱うこととなる.
このような大量のデータをメモリ上に保存し,計算処理に用いるためには,通常の単純変数では不可能である.

ここで一例として,キーボードから入力した 10 個の実数の平均と標準偏差を求めるプログラムを考えてみよう.

#include <stdio.h>
#include <math.h>

int main(void)
{
    // 10 個の float 型変数
    float a0, a1, a2, a3, a4, a5, a6, a7, a8, a9;

    // KBD 入力させる
    printf("a0 = ");    scanf("%f", &a0);
    printf("a1 = ");    scanf("%f", &a1);
    printf("a2 = ");    scanf("%f", &a2);
    printf("a3 = ");    scanf("%f", &a3);
    printf("a4 = ");    scanf("%f", &a4);
    printf("a5 = ");    scanf("%f", &a5);
    printf("a6 = ");    scanf("%f", &a6);
    printf("a7 = ");    scanf("%f", &a7);
    printf("a8 = ");    scanf("%f", &a8);
    printf("a9 = ");    scanf("%f", &a9);


    // 平均(average, mean)の計算
    float ave = 0.0;
    ave = ( a0 + a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 ) / 10.0;

    // 分散(variance)の計算
    float var;
    var = (a0-ave)*(a0-ave) + (a1-ave)*(a1-ave) + (a2-ave)*(a2-ave)      // 長いので途中で改行
        + (a3-ave)*(a3-ave) + (a4-ave)*(a4-ave) + (a5-ave)*(a5-ave)
        + (a6-ave)*(a6-ave) + (a7-ave)*(a7-ave) + (a8-ave)*(a8-ave) + (a9-ave)*(a9-ave);
    var /= 10;

    // 標準偏差(standard deviation)の計算
    float stdev = sqrt(var);

    // 結果の表示
    printf("average = %f, standard deviation = %f \n", ave, stdev);

    return 0;
}

このプログラムで正しい計算を行うことができる.
しかしよく見ると,このプログラムでは

など,似たような同種の処理が繰り返されている.
このような場合は,for, while などの「繰り返し文」を使いたいところだが, 変数名が a0, a1, ... , a9 と,それぞれ異なるため,繰り返し処理でうまく扱うことはできない.

さらに問題は,処理するデータ件数が10件ではなく5件,20件,100件と変化した時に,ソースコードを大幅に変更しなければならない.
これは煩雑になるだけでなく,記述ミスによるバグの温床になる.

そこで,複数の変数をまとめて 単一の識別子(=名前)で扱う方法が用意されている.
この機能のことを,「配列, array(アレイ)」と呼ぶ.

配列の使い方

配列の「定義」の仕方

配列変数を使用したい場合は,プログラム中において以下の様に記述する.

型名 配列名[個数];

例えば,整数型で,要素数10個の配列は

int a[10];

実数型で,要素数100個の配列は

float data[100];

とする.

配列変数の定義:

配列変数の使い方

配列変数をプログラム中で参照したり,値を代入するには,以下のように記述する.
基本的には普通の変数と同じである.

練習問題

要するに,「配列名」+「添字」で,一つの変数だとみなせば良い.

配列要素数のconst指定について

基本的には,配列の要素数は,変数として用意しておく. 以下の様に,変数 N は定数でなければならないが,最近の C99 規格に準拠したコンパイラでは,この制限が緩和されている.
ただし,コンパイル時に別途,オプションが必要な場合がある.

#include <stdio.h>

int main(void)
{
    // 古いコンパイラでは,
    // const int N=100;
    // としないとエラー.
    int N = 100;
    
    int aaa[N];

    return 0;
}

具体例1

10人分のテストの点数を格納する整数型の配列.

おすすめ:配列の要素数(ここでは10という定数)は,プログラム中の様々な場所で何度も使用するので, 以下のように定数変数(const 変数)として定義しておくと良い.

const int N = 10;    // N は配列の要素数. const は「定数」という意味
int score[N];        // 配列の定義

この場合,1個あたり 4 byteの int 型変数が 10個,計 40 byte の領域がメモリ上に確保される.

この配列を使って,何か処理をしてみよう.

score[0] = 100;   // 配列の  先頭に 100 を代入
score[1] = 50;    // 配列の 2番目に  50 を代入
.
.
.
score[9] = 90;    // 配列の  終端に  90 を代入

for(int i=0; i<N; i++) {     // i<10; ではなく i<N; と書くのがベター

    printf(" %d 番目のデータは,%d です.\n", i, score[i]);    // 全員の点数を画面に表示

}

Cの文法規則では,配列の要素番号は0から始まるので,配列の最後の要素は,score[10]ではなく,score[9] である.
したがって,要素数10の配列要素を先頭から順にアクセスするための繰り返し文は,for(i=0; i<10; i++)となる. この場合,iは10になってはいけない.

同様に 100 個の配列の最後の添字は 99 である.
一般に,要素数 N の配列要素の最後の添字は N-1 である.この値を超えてはならない.

以下のソースコードは,初めに示した平均と標準偏差を計算するプログラムを,配列を用いて書き直したバージョンである.
どの箇所が異なるか,比較してみよう.

#include <stdio.h>
#include <math.h>    // sqrt()用

int main(void)
{
    const int N = 10;    // const とは,値を変更できない変数であるという指示
                         // ここでは,データの個数を表すために使用

    float a[N];          // 配列の定義.N個のfloat型変数を記憶する場所が確保される

    int i;               // 繰り返し処理用のカウンタ変数

    for(i=0; i<N; i++) {    // N 回繰り返す
        printf("a[%d] = ", i);
        scanf("%f", &a[i]);
    }

    // 平均の計算
    float ave = 0.0;
    for(i=0; i<N; i++) {
        ave += a[i];
    }
    ave /= N;

    // 分散の計算
    float var = 0.0;
    for(i=0; i<N; i++) {
        var += (a[i] - ave) * (a[i] - ave);
    }
    var /= N;

    // 標準偏差の計算
    float stdev = sqrt(var);

    // 結果の表示
    printf("average = %f, standard deviation = %f \n", ave, stdev);

    return 0;
}

練習問題

具体例2

配列では数多くのデータを扱うことが多いので,その値を代入・設定する方法に工夫が必要である.
(実習課題で,以下のように複数のデータを入力させる問題がある場合は,この方法でおこなう.)

実行例:
Input 5 integers.
1 2 3 4 6      <-この行をキーボード等から入力
1 2 3 4 6      <-この行が出力される

#include <stdio.h>

int main(void)
{
    int i;
    const int N = 5;
    int a[N];

    printf("Input %d integers.\n", N);

    scanf("%d %d %d %d %d", &a[0], &a[1], &a[2], &a[3], &a[4]);

    //	配列を表示して確認
    for(i=0; i<N; i++) {
        printf("%d ", a[i]);
    }
    return 0;
}

通常は様々な配列サイズに対応できるよう,繰り返し文を使用する.

#include <stdio.h>

int main(void)
{
    int i;
    const int N = 5;
    int a[N];

    for(i=0; i<N; i++) {
        printf("a[%d]=? ", i);
        scanf("%d", &a[i]);
    }

    //	配列を表示して確認
    for(i=0; i<N; i++) {
        printf("%d ", a[i]);
    }
    return 0;
}

実行例1:1行にスペース区切りで入力

1 2 3 4 6      <-この行をキーボード等から入力

1 2 3 4 6      <-この行が出力される


実行例2:1行に1個数値を入力

1      <-この行から
2
3
4
6      <-この行までをキーボード入力,毎回Enterキーを押す

1 2 3 4 6      <-この行が出力される
リダイレクトによる入力

毎回数値を手入力するのは面倒なので,数値データなどを別ファイルなどに記録しておき,そこから「リダイレクト」と呼ばれる方法でデータをプログラム内の配列に流し込む方法もある.
以下の例では,あらかじめ in.txt というテキストファイルを作成しておき,ファイル内に書かれたデータを一度に配列に入力させることができる.

テキストファイル「in.txt」の中身をこのように設定し,.exeファイルと同じフォルダに置く.
1 2 3 4 6
実行方法(リダイレクト入力)

.\a.exe < in.txt

1 2 3 4 6      <-実行結果

配列使用のルールのまとめ

配列を使用するときの注意

1.配列の添字の範囲は,チェックはされない.

C言語では,処理効率を優先するため,配列の添字範囲が正しいかどうかのチェックは,プログラム作成者に委ねられている
したがって,配列の添字が有効範囲を超えてもコンパイラはエラーを出さない
さらに悪いことに,実行時にエラーとなることもあればエラーとならないこともある.また,一見うまく動作しているように見えても,偶発的にソフトウエアが落ちることがある.

以下のプログラムを実行して,動作を確認せよ.

配列要素を超えるとどうなるか,実行してみよう.for文中の 定数 1000 を 10000 や 100000に変更してみよう.
#include <stdio.h>

int main(void)
{
    const int N = 10;
    int x[N];      // 要素数 N 個の配列

    int i;         // カウンタ変数
    for(i=0; i<1000; i++) {     // 添え字の範囲は 0 から N-1(ここでは9) まで許されるが...

        x[i] = i*i;
        printf("x[%d] = %d \n", i, x[i]);

    }

    return 0;
}

練習問題

  1. 上記のプログラムを実行すると何が起こるだろうか.
  2. 上記のプログラムのどこがいけなかったのだろうか.
  3. どこをどのように直せばよいだろうか?
  4. 同じような誤りを犯さない様にするにはどうすればよいだろうか?

【ヒント】繰り返し文などの終了判定に,直接「定数」を置くのが間違いのもと!

2.配列から配列へのコピーは,繰り返し文で1つずつ

配列は,一つ一つの変数のリストにすぎず,配列の名称は,配列の先頭アドレス(=メモリー中の場所のこと)を示すラベルにすぎない.
C言語では,配列全体を一度にコピーする演算子が無いため,1つ1つ代入する必要がある.

配列全体の正しいコピー方法
const int N = 10;
int a[N], b[N];    // 配列が二つあるとする.

// a の中身を順に b にコピー
int i;
for(i=0; i<N; i++) {
    b[i] = a[i];    // 各要素を 1 個づつ代入 
}
以下は,全て誤った配列のコピー方法.
正しくないばかりか,プログラムが不正終了する場合がある.
b = a;          // ダメな例.

b[9] = a[9];    // 最後の要素一つがコピーされるだけ. 

b[10] = a[10];  // そもそも [10] というのがアクセス違反

配列の初期化

C言語の規格では,配列は(通常の変数と同様)定義しただけでは中身は「不定」と定義されている. 即ち,どのような値が入っているか規格上は決められていない.
したがって,使用方法によっては,あらかじめ 0 などの初期値を代入する必要がある.
以下は,実数型配列の中身を全てゼロに設定する例である.

// clear array : 配列のゼロクリア

const int N = ???;    //  N は要素数.いくつでも良い.
float array[N];       // 配列本体

int i;
for(i=0; i<N; i++) {
    array[i] = 0.0;     // ひとつずつ0を代入
}

配列中にゼロ以外の値を設定するためには,基本的には1つ1つの要素に値を代入する.

int array[5];   // 要素数5の配列変数の定義

array[0] = 1;    // 1個ずつ代入
array[1] = 2;
array[2] = 3;
array[3] = 4;
array[4] = 5;

しかし,配列の「定義」と同時に値を設定する場合には,以下のような初期化リストを使った「配列の初期化」を行うことができる.

// これはOK.代入ではなく初期化という.

int array[5] = { 1, 2, 3, 4, 5 };

さらに,初期化リストより配列の要素数が確定しているので[ ] の中の要素数を省略できる.

// この場合は,配列サイズ省略OK

int array[] = { 1, 2, 3, 4, 5 };

この場合,配列のサイズはコンパイラが自動的に決定してくれる.
しかし,配列の要素数はそのあとの処理で必要になるので,一般的には省略しないほうが良い

ここまでの「=」記号は,「配列の初期化」であり,(記号は同じであるが)代入ではない.
従って,以下のコード中の = はすべて代入とみなされるので,いずれもコンパイルエラーとなる.

int array1[5];                // 配列の定義

array1 = { 1, 2, 3, 4, 5 };    // これはエラー! 

array1[] = { 1, 2, 3, 4, 5 };  // これもエラー 

array1[0] = { 1, 2, 3, 4, 5 };  // もちろんこれもエラー

2次元配列

Cでは,1次元の配列だけではなく,2次元の配列を作ることもできる.
例えば,3x4の2次元配列を作るには,

int a[3][4];      // 2次元配列の定義

a[0][0] = 10;   // 代入例
a[1][2] = 5;

とする. この時の配列の形は,概念的には以下のようになる. 代数でいうところの行列によく似ている. 最初の[]が行を,後の[]が列を表している.

実際のコンピュータのメモリは1次元なので,あくまで概念的に2次元と考えられる,と理解すると良い.
(内部で2次元→1次元の添字の変換計算が自動的に行われる.)

2次元配列の初期化

2次元配列の場合も以下のとおり「定義と初期化」が可能である.

const int M = 4;
const int N = 3;

int array2D [M][N] = {{ 1,  2,  3},   // <- これで1行分
                      { 4,  5,  6},
                      { 7,  8,  9},
                      {10, 11, 12} };

ただし,要素数の省略は,最初の[]のみ,書かなくても許される.

const int M = 4;
const int N = 3;

int array2D [ ][N] = {{ 1,  2,  3},
                      { 4,  5,  6},
                      { 7,  8,  9},
                      {10, 11, 12} };

2次元配列にデータを読み込む方法

1次元配列と同様,2次元配列にデータを格納するには以下のように2重ループを用いる. 添字の順序に注意して記述する.

#include <stdio.h>

int main(void)
{
    const int M = 4;
    const int N = 3;
    
    int a[M][N];

    int i,j;
    for(i=0; i<M; i++) {
        for(j=0; j<N; j++) {
            scanf("%d", &a[i][j]);    // a[i][j] で,1つの変数と見れば良い.
        }
    }

    //	配列を表示して確認
    for(i=0; i<M; i++) {
        for(j=0; j<N; j++) {
            printf("%d ", a[i][j]);
        }
        printf("\n");           // 1行分表示したら改行
    }
    return 0;
}

実行例1

1 2 3        <- この行から
4 5 6
7 8 9
10 20 30     <- この行までをキーボード入力,1行入力するごとにEnterキー

1 2 3        <- 出力
4 5 6 
7 8 9 
10 20 30

実行例2:行-列の順に,1行にまとめて入力
1 2 3  4 5 6  7 8 9  10 20 30        <- 1行で入力

1 2 3        <- 出力
4 5 6 
7 8 9 
10 20 30

配列を関数へ渡す方法

配列を引数にとる関数の作り方・使い方は,非常に重要.

関数の引数として配列を渡す場合,配列本体とあわせて,要素数を一緒に渡すのが基本である.
(∵C言語では,配列は自動的に「アドレス渡し」となる仕様で,呼び出された関数には配列の「先頭の場所」しか渡されないため,合わせて配列サイズがわからないと終端が判別できない.)

具体的には,関数の引数に配列を渡す場合は,以下のように記述する.

// 数値の配列を関数に渡す方法

#include <stdio.h>

// 関数で配列を受け取る方法.
// ここで n は配列の要素数
// 配列本体と要素数を引数に取ることにより,どんな要素数の配列にも対応できる.

void disp_array(int data[], int n )     // [] のみでOK.
{
    int i;
    for(i=0; i<n; i++) {
        printf("data[%d] = %d \n", i, data[i] );
    }
}

int main(void)
{
    const int N = 5;                  // 配列の要素数
    int a[N] = {2, 5, 13, 14, 15};    // 配列

    disp_array( a, N );    // 関数呼び出し,配列を渡す

    return 0;
}

別の方法として,配列の要素数をグローバル変数として準備しておく方法もある.
ただし,配列の種類や個数が増えてくると,どの変数がどの配列の個数かわかりにくくなるので,いろいろなサイズの配列がある場合は望ましくない.

// 配列を関数に渡す方法.その2

#include <stdio.h>

const int N = 5;    // 配列の要素数をグローバル変数として定義

// 配列のみ渡す
void disp_array(int data[])
{
    int i;
    for(i=0; i<N; i++) {
        printf("data[%d] = %d \n", i, data[i] );
    }
}

int main(void)
{
    int a[N] = {2, 5, 13, 14, 15};    // 数値の配列

    disp_array( a );    // 関数呼び出し

    return 0;
}
注意:C言語の仕様では,関数に渡す配列は「アドレス渡し」となり,呼び出した関数内で配列の内容を変更すると,呼び出し元の配列の内容も変更される!

まとめ

Q and A

Q and A
配列の添え字は1からでなくゼロから始まるのはなぜですか?
配列の添え字は,配列の「先頭アドレスからのオフセット」を表し,先頭はオフセット==0であるからです.ポインタの回も参照してください.
配列を関数に渡す場合はアドレス渡しなのに,&や*は不要なのですか?
不要です.&や[]をつけてはいけません.[]を付けない配列名自身はアドレスであるためです.
配列を関数に渡すとアドレス渡しとなることはわかりましたが,関数に配列の中身を変更させたくないときはどのようにすればよいですか?
関数の仮引数に constをつければOKです.void disp_array(const int data[], int N)
配列に一括で値を(初期化ではなく)代入する方法はありますか?
基本的にはできません.繰り返し文を使って,1個1個,代入します.
ただし,最近のC++規格では可能となっています.

練習問題

以下の問それぞれに対応するプログラムを作成しなさい.

  1. キーボードから整数を10個入力して配列に順次格納し,正順および逆順で画面に表示せよ.

    #include <stdio.h>
    
    int main(void)
    {
        const int N = 10;     // 配列の要素数
        int array[N];       // 整数 N 個分の配列
        
        // データ入力
        for(...) {
    //      printf(...);    // 必要に応じて
            scanf(...);
        }
    
        // 配列データを正順に出力
    
        // 配列データを逆順に出力
    
        return 0;
    }
    
    
    実行例:
    1 0 6 2 5 3 7 9 8 4     <- キーボードから入力 またはファイルからリダイレクト
    
    1 0 6 2 5 3 7 9 8 4     <- 正順で出力
    4 8 9 7 3 5 2 6 0 1     <- 逆順で出力
    
  2. キーボードから実数を10個入力し配列に格納し,平均値を指定の桁数で画面に表示せよ.
    可能であれば,配列の合計値(または平均値)を計算する部分は,関数にしてみよう.

    ヒント:合計値を記憶しておく変数を別途用意し,0で初期化しておき,そこに足してゆく.

    #include <stdio.h>
    
    int main(void)
    {
        const int N = 10;     // 配列の要素数
        double data[N];       // 実数 N 個分の配列
        
        // データ入力
        for(...) {
    //      printf(...);    // 必要に応じて
            scanf("%lf", &...);    // double には %lf
        }
    
        // 合計値の計算
        double sum = 0.0;
        for(...)
            ...
    
        // 結果の出力
        printf(" ??? ", .... );
        return 0;
    }
    
    
    実行例:(結果の数値は正しいとは限らない)
    
    1 2 5 3 10 9 0.9 1.1 2.2 0.1     <- キーボードから入力 またはファイルからリダイレクト
    5.320                            <- 小数点以下3桁で出力
    

  3. 0から100までの整数値をランダムに10個生成して配列に格納,そのなかから最大値を探し,値を画面に表示せよ.

    注1:ランダムな整数値(乱数と呼ぶ)の生成には,ライブラリ関数であるrand()を用いる.参考までに,以下の関数 gen_rand()を用いてよい.
    注2:渡された配列内の値の最大値を探索し返す関数を作成する.暫定1位を記憶しておく変数maxを用意する.

    #include <stdio.h>
    #include <time.h>     // time()用
    #include <stdlib.h>   // rand() 用
    
    //
    // 0~100 の乱数を生成して配列に代入する関数
    //
    void set_rand(int a[], int n)    // a はアドレス渡し,nは配列サイズ
    {
        srand(time(NULL));  // 乱数を初期化 
    
        for(int i=0; i<n; i++) {
            a[i] = rand()%101;      // rand()は呼ばれるたびに整数の乱数を返す.
    //      printf("%d ", a[i]);    // 確認用
        }
    //  printf("\n");    // 確認用
        return;
    }
    
    //
    // 配列の中から最大値を見つけて,その値を返す関数
    //
    int find_max(...)
    {
        int max;       // ここに最大値を格納して返す.
    
        // 最大値を探す処理
        ????
    
        return max;
    }
    
    int main(void)
    {
        const int N = 10;
        int a[N];   // 整数 N 個分の配列
    
        // データ入力
        set_rand(a,N);
    
        // 結果の出力
        printf("max = ...",  find_max(???) );
        return 0;
    }
    
    実行例:(乱数の値は毎回異なる.)
    
    55 82 57 62 4 81 19 49 0 26     <- 確認の為の表示
    max = 82
    
    
  4. 配列の数値データを降順(大きい順)に並べ替えて,前後の値を画面に表示せよ.
    このような処理を「ソーティング」という. 基本的に二重のループ処理となる.

    配列のソーティング部分については関数にしてみよう.

    #include <stdio.h>
    
    void sort_array(...)
    {
        ...
    }
    
    int main(void)
    {
        // 配列サイズや値を変更しても正しく動作すること!
        const int N = 5;
        int a[N] = {3, 2, 5, 4, 1};
        
        // 最初の配列データの出力
        printf("Before :");
        for(...) {
            printf(" ...", ???);
        }
    
        // 並べ替え.まずはここでテストして,関数呼び出しに変更する.
        for(...) {
            for(...) {
                ...
            }
        }
        //    sort_array(...);
    
        // 結果の出力
        printf("After  :");
        for(...) {
            printf(" ...", ???);
        }
        return 0;
    }
    
    実行例
    Before : 3 2 5 4 1
    After  : 5 4 3 2 1
    
    並べ替え(ソーティング)について

    配列データの並べ替えには,さまざまなアルゴリズムが提案されている. 一番単純な降順の並べ替えは,以下のような手順で実現可能である.
    ここでは配列名を a,要素数をN とする.

    1. 配列の k 番目の値に注目する.(最初は k=0 とする.)
    2. k 番目の値 a[k]と,それ以降の配列の値 a[l], l=k+1, k+2, ... , N-1 とを順次比較.
      a[l]a[k]より大きければ,a[k]の値とa[l]の値を入れ替える=swap().l=N-1の時点でa[k]の値が最大値となっている.
    3. これを,k=0 ∼ N-2まで繰り返す.

    この手順を繰り返し,全てのkに対する処理が完了した時点で,配列の先頭から大きい値が順に並んでいる.
    また,上記の「大きければ」の比較を「小さければ」にすれば,昇順(小さい順)の並べ替えができる.

    このほかにもさまざまなソーティングアルゴリズムがあるので,興味があれば各自で調べてみよう.