配列

アプリケーションでは多くの値を処理する状況は頻繁に発生します。
例えば、中間テストの平均点を求めるアプリケーションを作成するとしたならば、生徒の数に等しい「点数」を表す変数が必要になるでしょう。

    // 生徒の数だけ変数を宣言する必要がある
    char score1 = 80;
    char score2 = 46;
    char score3 = 70;
    char score4 = 70;
    // (中略)
    char score30 = 52;

また、どんなクラスでも使用できるような汎用性の高いアプリケーションとするならば、生徒数がクラスによって異なることも考慮しなくてはなりません。

配列はこのようなケースで効率よくプログラムを作成し、実行できるような仕組みを提供します。

配列とメモリ

C言語において変数が宣言されると、変数の項で学習したようにメモリ上に一定の領域が確保されます。
この領域の位置(アドレス)は変数名に保存され、サイズは変数の型で決まったはずです。
しかし、複数の変数を宣言した場合にメモリの領域が隣り合わせになるとは限りませんでした。
従って大量の変数をメモリに確保すると、その都度確保する処理を行わなくてはならず、メモリ上のあちこちに配置されている可能性があるので実行速度の面でもメモリの効率的な使用という意味でも課題が残ります。
特にC言語が開発された頃のコンピュータではメモリと実行速度は大きな課題でありました。

そこで配列はメモリ上に確保する時に「メモリ空間に同じサイズの領域を連続して確保」します。
例えば2バイトのchar型で50個の配列であるならば、メモリは最小限の200byteで足ります。
また、配列のアドレスは先頭のみを記憶しておき、そこから何バイト目を参照するとすれば、無駄に膨大なメモリ空間を検索する必要もありません。
このように配列は様々メリットを生み出します。
しかし、「メモリ空間に同じサイズの領域を連続して確保」する為に制限もあります。

1つ目は、異なる型の変数は配列として扱えません。
1番目が4bteで2番目が2byteというように最小のメモリを確保していけば、メモリはさらに節約できるのですが配列のアドレス(格納場所)を検索する為の処理が複雑になります。
しかし、通常は同じような項目を配列とする(例で言えば、生徒の点数は0〜100に収まるのでcharで充分)ので問題はありません。

2つ目の制限は、宣言時に配列の個数(配列のサイズ)を指定しなくてはならないということです。
とはいえ、個別に宣言した場合でも個数は最初に必要になる為、大きな制限ではないでしょう。
しかし、配列のサイズを可変にしたい(出し入れしたい)という用途に使う場合は、その都度に配列を宣言しなくてはならないでしょう。

配列

次のプログラムは、5人の生徒の数学の点数の平均を求めるプログラムです。
[example05.c]

#include <stdio.h>

int main(int argc, char *argv[])
{
    int score[5];
    score[0] = 80;
    score[1] = 65;
    score[2] = 50;
    score[3] = 70;
    score[4] = 50;
    int i;
    int total;
    for(i = 0; i < 5; i++)
    {
        total = total + score[i];
    }
    printf("平均点は%dです。", total / 5);
    return 0;
}

このプログラムでは最初にint型でサイズが5の配列scoreを宣言しています。

    int score[5];

このように配列の宣言は、

型名 配列名[要素数];

で行われます。
この行が実行されるとき、メモリ上にint型(4byte)の領域が5個連続で確保されます。
次の5行では配列の各要素に値を代入しています。

    score[0] = 80;
    score[1] = 65;
    score[2] = 50;
    score[3] = 70;
    score[4] = 50;

このように配列の要素を使う場合は、[]の中に添え字を使いn番目の要素という形で参照します。
注意して欲しいのは、このn番目の値(インデックスと呼ぶ)は0から始まるということです。
よって、サイズが5である場合は最後の要素のインデックスは5ではなく4になるので注意してください。
では5番目の要素を使用してしまうとどうなるのでしょうか?

この答えは解りません。
何故ならば配列の隣にどんな変数が格納されているかは解らないからです。

配列に添え字をつけて使用すると、宣言した型の変数として使用できます。
すなわち、int型であればint型の制約と演算をすべて行えるわけです。

次に平均を求める為に、まずは全要素(全生徒の点数)の合計を求めています。

    int i;
    int total;
    for(i = 0; i < 5; i++)
    {
        total = total + score[i];
    }

先日学んだfor文を使い配列の要素数の繰り返しを行っていることが解るでしょう。
配列の添え字には変数も使用できるため、ループ用のカウンターiを用い、0番目の要素から順にtotalに加えています。
この形はいわゆる配列のループと呼ばれるもので頻繁に使われます。
カウンターを0から始め、配列の要素は0から始めることにより、forループの終了条件が、「i < 配列の要素数」とすっきり書けているところは美しいですね。

配列の宣言

example05.cでは配列の宣言と各要素の代入を別途記述しましたが、変数の宣言と代入を同時に行えるように、配列もまた宣言と代入を同時に行うことができます。

    int score[5] = {80, 65, 50, 70, 50};

このように配列の値として、{}(大括弧)でまとめて値を宣言しています。
各要素の区切りには,(カンマ)を使用しています。
また、配列の宣言と代入をまとめて行う場合は、配列の要素数を省略することも可能です。

    int score[] = {80, 65, 50, 70, 50};

この場合、scoreという配列は暗黙的にサイズ5で宣言されます。
配列の要素があらかじめわかっている場合は、この書き方をとると便利です。

配列の要素数

もう少しだけexample05.cを見ていきましょう。
少々気になるのは、配列のサイズである5がコードのあちらこちらに定数として書かれている点です。
もし、生徒数が変わったならばコードのあちらこちらの配列数を変更しなくてはならない為、少々面倒です。
そこで、配列の要素数を取得するには次のように記述します。

    配列の要素数 = sizeof(配列名) / sizeof(配列名[0]);

sizeof演算子を使うことで配列や変数などのメモリのサイズを取得できます。
全体のサイズを1要素のサイズで割ると配列のサイズが逆算できるわけです。

この仕組みを使ってexample05.cを書き直すと、次のようになります。

#include <stdio.h>

int main(int argc, char *argv[])
{
    int score[] = {80, 65, 50, 70, 50};
    int i;
    int total;
    int size = sizeof(score) / sizeof(score[0]);
    for(i = 0; i < size; i++)
    {
        total = total + score[i];
    }
    printf("平均点は%dです。", total / size);
    return 0;
}
サイズ0の配列

もし、要素数が0である配列があるとすれば、次のように宣言できます。

    int array[0];
    または
    int array[] = {};

実際に簡単なソースコードを書いてコンパイルしてみれば解りますが、これらの宣言はエラーにはならずにコンパイルが通るでしょう。
当然ながら、サイズが0であるので、メモリ上のサイズもまた0です。
sizeof(array)はゼロが返ってくるでしょう。
しかし、array[0]にアクセスするようなプログラムを書いてみると、実行時にエラーとなるはずです。
すなわち、

    配列の要素数 = sizeof(配列名) / sizeof(配列名[0]);

という形は配列のサイズが1以上である場合にしか使用できません。
サイズ0の配列に意味があるのかはここでは触れませんが、意味を考えてみましょう。