関数


関数

一般に、プログラム中で似たような処理を何度も 記述するような場面では、それを「関数」として 定義してそれを呼び出すようにし、似たような処理を プログラム中の複数の箇所に記述することは避けるべきである。 また、たとえ一回しか使わなくても、長大なプログラムは どこで何をしているのか見通しが悪くなるので、 プログラム中のまとまった処理単位を関数にして分離することは プログラムの可読性を向上させる。

次のプログラムは、n個のものからr個のものを選び出す場合の 組合せの数 \({}_nC_r\) \(\displaystyle \frac{n!}{r!(n-r)!}\) で計算するものである。

#include <stdio.h>

int main(void) {
    int n, r;
    int f_n, f_r, f_nr;
    int i;

    scanf("%d", &n); 
    scanf("%d", &r); 
    
    f_n = 1;
    for (i=1; i<=n; i++) {
        f_n *= i;
    }

    f_r = 1;
    for (i=1; i<=r; i++) {
        f_r *= i;
    }

    f_nr = 1;
    for (i=1; i<=n-r; i++) {
        f_nr *= i;
    }
    
    printf("%d\n", f_n / f_r / f_nr);

    return 0;
}
combination.c

これを見ると、階乗を計算している同じような部分が3箇所あるが、 これを関数を使って書き直したものを次に示す。

#include <stdio.h>

int factorial(int n) {
    int i, r;

    r = 1;
    for (i=1; i<=n; i++) {
        r *= i;
    }

    return r;
}

int main(void) {
    int n, r;

    scanf("%d", &n);
    scanf("%d", &r);
    
    printf("%d\n", factorial(n) / factorial(r) / factorial(n - r));

    return 0;
}
combination2.c

こちらの方がよりプログラムがすっきりしていて、意図も分かりやすく なっている。

注意 ここでは、関数factorialを関数mainより先に記述している。 C言語では上から下に順に翻訳(compile)するので、 「使われるものを先に定義し、使うのは後」だと問題が起きない。 なお、分割コンパイルとリンクで後で説明する 「プロトタイプ宣言」を行えば、逆でも大丈夫。
関数宣言の一般形は、

    戻値の型 関数名(引数1の型 引数1, 引数2の型 引数2, ...) {
        文
        ...
    }

の通りである。このように、引数は複数存在する場合もある。 次の関数は、2つの引数の最大公約数を計算するものである。

    int gcd(int n, int m) {
        while (n != m) {
            if (n > m) {
                n = n - m;
            } else {
                m = m - n;
            }
        }

        return n;
    }

また、引数が存在しないこともある。 次の関数は、1から6までの乱数を発生させるものである。

    int dice(void) {
        return (int)(rand()/(1.0+RAND_MAX) * 6) + 1;
    }

このように、引数が存在しない場合は括弧内にvoidと書く。

注意1 rand()は、0 ≤ x ≤ RAND_MAX の整数の いずれかをランダムに、等しい確率で出力する(一様分布)組み込み関数である。

注意2 rand()を用いてサイコロのように振舞わせるには、 ややこしいが上で行っているように、

という手順がよい。rand() % 6で簡単に0, 1, 2, 3, 4, 5が均等に出現する 乱数が得られそうなものだが、乱数の生成アルゴリズムの関係で、このように 生成した乱数は周期が短いなど、乱数としての性質が悪くなることが多い。

注意3 rand()を使うには、プログラムの冒頭で #include <stdlib.h>が必要。

注意4 このままだと毎回 同じ乱数系列を発生してしまうので、乱数系列を発生させる前に srand(time(NULL));で初期化することがよく行われる (rand1.c, rand2.c)。 time(NULL)は現在時刻を1970年1月1日から現在までの経過秒として 返す関数である。また、ここで使っているtime関数を使うには、 プログラムの冒頭で#include <time.h>が必要。

また、戻値がない関数もある。 次の例は、「-」の文字をn個表示して改行する関数である。

    void writeline(int n) {
        int i;

        for (i=0; i<n; i++) printf("-");
        printf("\n");
    }

このように、戻値の無い関数の場合は戻値の型の代りにvoid と書いておく。

関数から戻値を返すには、上の例で分かるように、「return 式;」 と書けば良い。戻値のある関数の場合は、「return 式;」が 関数の定義中に少なくとも一つ必要である。 return文は関数の末尾とは限らず、複数置く ことも出来る。 次の例は、与えられた数が素数か否かを判定するものである。

    int isprime(int n) {
        int i, max;

        if (n <= 1) return 0;
        if (n == 2) return 1;
        max = (int)ceil(sqrt((double)n));
        for (i=2; i<=max; i++) {
            if (n % i == 0) return 0;
        }
        return 1;
    }

このように、関数の途中にreturnがあった場合はそこで 関数を抜けて式の値を返す。 戻値のない関数の場合はreturn文は無くても良いが、 途中で関数の実行を終了したい場合は、ただ「return;」と 書けば良い。

ローカル変数とグローバル変数

ところで、上で挙げた組合せの数を計算するプログラムを もう一度見てみる。

#include <stdio.h>

int factorial(int n) {
    int i, r;

    r = 1;
    for (i=1; i<=n; i++) {
        r *= i;
    }

    return r;
}

int main(void) {
    int n, r;

    scanf("%d", &n);
    scanf("%d", &r);
    
    printf("%d\n", factorial(n) / factorial(r) / factorial(n - r));

    return 0;
}
combination2.c

ここで、

という変数が使われているが、両方で同じ変数名を使って正しく動くのは 不思議であろう。例えば、n=5, r=3でスタートした場合、 最初のfactorial(n)の呼出しでr=120になってしまい、 次のfactorial(r)の呼出しで本来3!を計算すべき ところが120!を計算することになってしまうと思われる。

実際にはこのような心配は無用で、各関数内で宣言された変数 (と、引数。上記の場合はfactorial内のn) はその関数内だけで有効で、他の関数でその値を読んだり書いたり することは出来ない。また他の関数で同名の変数が使われていても それは互いに別の変数として扱われる。 このような変数を一般に「ローカル変数」という。 ある程度大規模なプログラムを書く場合は、 他の関数でどんな名前の変数を使っているか気にする必要が無いので 大変便利な機能である。すなわち、

というメリットがある。

なお、「ローカル変数」と対になる単語として、どの関数からでも 使うことが出来る変数という意味の「グローバル変数」というものが ある。 これを使うとプログラムが読みにくくなる可能性が高いので、 明らかに使った方がよい場合を除いて、普段は使わないように 努力するべきである。関数間でデータを受け渡しする場合は、 引数として渡せば良い。

グローバル変数は、関数の定義の外で、

    変数の型名 変数名;

のように宣言すれば使うことが出来る(下の例参照)。

#include <stdio.h>

int a;

void func(void) {
    int b;

    a = 3;
    b = 4;
}

int main(void) {
    int b;

    a = 1;
    b = 2;
    printf("%d\n", a);
    printf("%d\n", b);
    func();
    printf("%d\n", a);
    printf("%d\n", b);

    return 0;
}
localtest.c

この例で、aはグローバル変数、bはローカル変数で ある。実行結果は、

1
2
3
2
となり、ローカル変数とグローバル変数の違いが分かると思う。

K & Rスタイル

C言語はANSI-C以前と以後で関数の宣言の仕方が違っており、 古くは次のように書いた。
int factorial(n)
    int n;
{
    int i, r;

    r = 1;
    for (i=1; i<=n; i++) {
        r *= i;
    }

    return r;
}

この古いスタイルを K & Rスタイルと呼ぶことがある。 今のコンパイラでも一応コンパイルできるが、今更このスタイルで 書くことは無いであろう。

main関数

mainも関数の一種である。他の名前の関数との違いは、 プログラムの最初に呼ばれるという約束があることだけである。
int main(void) {
    ...
    return 0;
}
mainintを返す関数であり、 returnする値は プログラムが正常に処理を出来たら0、何らかのエラーが あったら1とするのが一般的。

(詳細は「コマンドライン引数」の説明のときに再度述べる。)


関数