配列と関数

引数に配列を持つ関数

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

#include <stdio.h>

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[]」は、配列を引数に取ることを表しています。

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

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

第二引数のlengthは第一引数の配列の要素数を指定します。
実は関数の引数に配列を渡した場合、関数内ではその配列の要素数を知る手段がありません。
配列の要素数を知るにはサンプルコード22行目のようにsizeof演算子を利用する方法がありますが、引数として渡された配列にsizeof演算子を使用しても配列のバイト数を得ることはできません。
そのため、引数を別途用意して配列の要素数を関数呼び出し側から渡しているのです。

関数内での処理は単純です。
for文で配列内の数をすべて加算し、最後に配列の要素数で割れば配列の平均値を得られます。
それをreturn文で返すだけです。
int型同士の除算はint型になりますから、double型にキャストすることを忘れないようにしましょう。

また、簡単なエラー処理として、配列の要素数が0以下の場合には0を返して関数を抜けるようにしています。
(5、6行目)

関数の独立性

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

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

main関数内でのsizeof演算子による配列の要素数の計算は関数の独立性には関係ありませんが、こうしておくと配列の要素数がいくつであっても対応できるようになります。
つまり配列の要素数を修正することになってもこの箇所は修正せずに済むということです。

多次元配列の引数

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

#include <stdio.h>

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

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

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

関数で配列を得る

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

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

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

#include <stdio.h>

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

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

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

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

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

と覚えておきましょう。