配列と関数

引数に配列を持つ関数

関数の引数に配列を指定することもできますが、いくつか注意点があります。


#include <stdio.h>

//長さlengthの配列arrの要素の平均を返す
double Average(const int arr[], int length)
{
    if (length <= 0)
        return 0.0;

    int total = 0;
    for (int i = 0; i < length; i++)
    {
        total += arr[i];
    }

    return (double)total / length;
}

int main()
{
    int numbers[] = { 60, 75, 84 };
    printf("%f", Average(
            numbers,
            sizeof(numbers) / sizeof(numbers[0])));
    getchar();
}
73.000000

自作関数Averageは、引数として受け取ったint型配列の平均値をdouble型で返す関数です。
第一引数のconst int arr[]は、配列を引数に取ることを表しています。
第二引数lengthは、第一引数の配列のサイズです。

処理自体は単純で、配列の要素をすべて加算した後に配列のサイズで割ることで平均値を求めています。
平均値ですから、戻り値はint型ではなくdouble型で求めています。
(型変換を参照)

constは「定数」を意味するキーワードで、この場合は「この配列は関数内で書き換えられない」という意味となります
constを書かなくても動作はしますが、関数内での書き換えが起こらないことを保障する意味でも付けたほうがいいでしょう。
(詳しくは定数とマクロの項で説明します。)

配列の要素数

仮引数に指定する配列の添字(要素数の指定)は必要ありません。
一次元配列の場合は、添字を書いても書かなくても同じ意味になります。

引数として渡された配列は、関数内でそのサイズを知る手段がありません。
以前説明したsizeof演算子は、同じ関数内で宣言した配列の要素数を得ることはできます。
しかし関数の引数で渡された配列のサイズは取得できません。

実は関数の引数に指定された配列は、配列のように見えますがポインタという別物として扱われます。
配列のサイズが指定できない、取得できないのはこのためです。
ポインタについてはここでは触れませんが、配列の要素数が必要な場合は別の引数で要素数を渡す必要があります。

関数の独立性

このコードでは配列の要素数は3であることは明らかなので、わざわざ引数で要素数を指定せずにコード中にそのまま「3」と書けばいいじゃないか、と思われるかもしれません。
しかし、そのような関数は要素数が「3」以外の配列では対応できません。
要素数が4や5などの配列の処理をするにはコードを修正したり、また新たな関数を定義したりする必要があり、非効率です。

関数は処理を独立させて他の場所から再利用できるようにする目的があります。
処理したい配列の要素数に関わらず共通して使用できる関数にしたほうが効率的にプログラミングができます。

なお、main関数内でのsizeof演算子による配列の要素数の計算は関数の独立性には関係ありませんが、配列の要素数を修正した場合でもこの箇所は修正せず要素数を得ることができます。

多次元配列の引数

関数は多次元配列を引数に取ることもできます。


#include <stdio.h>

//長さlengthの二次元配列arrの要素のうち
//一番大きな数字を持つ要素番号を返す
int GetWinner(const int arr[][5], int length)
{
    if (length <= 0)
        return -1;

    int win = 0;
    int win_total = 0;

    for (int i = 0; i < length; i++)
    {
        int tmp = 0;
        for (int j = 0; j < 5; j++)
        {
            tmp += arr[i][j];
        }
        if (tmp > win_total)
        {
            win = i;
            win_total = tmp;
        }
    }

    return win;
}

int main()
{
    int numbers[][5] = {
        { 60, 75, 82, 56, 70 },
        { 64, 66, 74, 70, 72 },
        { 62, 72, 66, 68, 80 }
    };

    int winner = GetWinner(
        numbers,
        sizeof(numbers) / sizeof(numbers[0]));
    printf("1位: %d番", winner);

    getchar();
}
1位: 2番

関数GetWinnerは、合計点が最も高い二次元配列の先頭要素番号を返す関数です。
(ただし同点は考慮していません)

引数は一次元配列の時とほとんど同じで、一次元配列が二次元配列になっているだけです。

引数に配列を指定する場合、先頭の要素のみ要素数を省略することができます。
一次元配列の場合は先頭要素しか存在しないため、要素数を省略できたわけです。
これは配列の初期化時の動作と同じです。

二次元目以降の要素数は省略できません。
この関数は二次元目の要素数が5の配列以外には使用できない、ということになります。

関数から配列を得る

関数の戻り値として配列を受け取れれば便利ですが、実はC言語では関数の戻り値に配列を指定することはできません。
関数によって配列を得たい場合は別の方法を使用します。

引数で配列を受け取り、書き換える

最初のサンプルコードでは、引数の配列には「const」を付けて書き換えができないように指定していました。
逆に言えば、constを付けなければ関数内で配列を書き換えることができるということです。


#include <stdio.h>

//長さlengthの配列arrを昇順に並べ替える
void Sort(int arr[], int length)
{
    int tmp;

    for (int i = 0; i < length - 1; i++)
    {
        for (int j = length - 1; j > i; j--)
        {
            if (arr[j - 1] > arr[j])
            {
                tmp = arr[j - 1];
                arr[j - 1] = arr[j];
                arr[j] = tmp;
            }
        }
    }
}

int main()
{
    int numbers[] = { 60, 75, 82, 56, 70 };
    int indexes = sizeof(numbers) / sizeof(numbers[0]);
    Sort(numbers, indexes);
    for (int i = 0; i < indexes; i++)
    {
        printf("%d\n", numbers[i]);
    }
    getchar();
}
56
60
70
75
82

関数Sortは、数値の小さい順に配列の中身を並べ替える関数です。
これはバブルソートという手法を用いた並べ替えですが、今回は処理の解説はしません。
興味がある人は動作をひとつずつ追ってみてください。

26行目の関数Sortの呼び出しによって、main関数内で宣言した配列numbersの値が書き換わっていることを確認してください。

このように、配列は関数の引数に指定すると関数内で書き換えられる可能性があります。
配列以外の通常の変数は関数内で書き換えられる可能性はありません。

その理由はポインタを学習しないと説明することができません。
とりあえず今は、

  • 配列を関数に渡すと、関数内で値が書き換えられる可能性がある。
    (関数内での書き換えが呼び出し元にも影響する)
  • 引数にconstが付いていれば書き換えられることはない。
    書き換えようとするとコンパイルエラー。
  • 変数は、関数内で値が書き換えられることはない
    (書き換えても呼び出し元には影響しない)

と覚えておきましょう。