2006 年度「計算機基礎論3B」 2006-12-22

§29 関数の自作

(29.1) C言語では、関数を自分で作り、元からある関数と全く同じように使うことができる。 K&R2 §1.7, 1.8 参照。

(29.2) 次のプログラムはループを利用して 16 の 3 乗と 3 の 16 乗を計算するだけのプログラムである。 すでに学んだことしか使っていない。 (「for (i = 1; i <= b; i++)」 と書かないのは、単なる習慣らしい。一文字だけ短くて済むから?)

#include <stdio.h>

main() {
    int a, b, i, p;

    a = 16;
    b = 3;

    p = 1;              /* a の b 乗を計算し結果を p に収める */
    for (i = 0; i < b; i++) {
        p = p * a;
    }
    printf("%d の %d 乗は %d です.\n", a, b, p);

    p = 1;              /* b の a 乗を計算し結果を p に収める */
    for (i = 0; i < a; i++) {
        p = p * b;
    }
    printf("%d の %d 乗は %d です.\n", b, a, p);
}

(29.3) 上のプログラムの、 ベキ乗を計算する部分を関数として独立させたのが次である。

#include <stdio.h>

int power(int base, int n);     /* プロトタイプ宣言 */

main() {
    int a, b;

    a = 16;
    b = 3;

    printf("%d の %d 乗は %d です.\n", a, b, power(a, b));
    printf("%d の %d 乗は %d です.\n", b, a, power(b, a));
}


int power(int base, int n) {    /* 以下、関数 power() の定義 */
    int i, p;

    p = 1;
    for (i = 0; i < n; i++) {
        p = p * base;
    }
    return p;
}

(29.4) main() の終わりまでは、 「プロトタイプ宣言」とコメントをつけた行を除けば、 いままでの知識で理解できる。 ただし、power(x, y)xy 乗を計算する関数であると思って読むこと。

(29.5) 「以下、関数 power() の定義」以下で、 p = 1; とその次の for ループは(本質的に) 前のプログラムにあったものと全く同じである。

(29.6) 多くの関数は、cos(0) が 1 であるように、値をもっている。 その値のことを「返り値(かえりち)」と言う。 「int power」の int は、 power() の返り値が int 型であることを示している。 (数学のことばづかいを借りて言えば「整数に値をとる関数」ということ。)

(29.7) 関数は power(x, y) のようにして使うが、 この xy のことを「引数(ひきすう)」と言う。 ここでは x, yint 型と決めたので、 「関数 power()int 型の引数を二つとる」と言ったりする。 x, y でもいいのだが、 引数の意味がわかりやすいように、 最初の引数を base, あとの引数を n と呼ぶことにしよう。 「int power」 のあとの小カッコの中には引数の型と名前を、 間をカンマで区切って書く。 この例では第一引数は int 型で名前は base, 第二引数も int 型で名前は n, ということになる。

(29.8) その次の行の「int i, p;」は、 関数 power() の中で使う変数の宣言である。 いままで main() の中で宣言していたのと全く同じスタイルである。

(29.9) そのあとの部分で basen 乗を計算し、 最後の「return p;」でその値を返す。 return のうしろに書いた式の値がこの関数の値になる、 という約束である。

(29.10) 「プロトタイプ宣言」の行。 ここには、power() の定義の一行目と同じものを書く。 ただし、最後の中カッコ開き「{」を取り除き、 代わりにセミコロン「;」をつける。 これはコンパイラに 「これから出てくる power() はこういう関数 (int 型の引数を二つとって int 型の値を返す関数) です」と教える働きを持つ。

(29.11) 「power(a, b)」などとしてその値を計算させることを 「関数 power() を呼び出す」とも言う。

(29.12) プログラムは main() の最初から実行されるが、 関数を呼び出すところにくると main() の実行はいったん止まり、 その関数が最初から実行される。 関数から return で戻れば、 呼び出したところの次から、main() の続きが実行される。 自作関数から別の自作関数を呼び出す場合も同様。

(29.13) この例では、まず、 a が 16, b が 3 で power(a, b) が実行される。 呼び出された power() の側では base が 16, n が 3 で実行が始まる。 次に、 a, b の値はそのままで power(b, a) が実行される。 呼び出された power() の側では base が 3, n が 16 で実行が始まることになる。

(29.14) main() の中で定義した変数と関数 power() の中で定義した関数は、 たとえ名前が同じでも、全く無関係である。 たとえば、仮に main() の中に i という名前の変数があったとして、 power() の中で i の値が変わっても、 main() 側の i は変わらない。

(29.15) 言い方を変えれば、main() の中だけで使う変数は main() の中カッコの中で定義する。 その他の自作関数の中だけで使う変数はその中カッコの中で定義する。 これらは名前がダブっていても別物である。 それとは別に、main() の前で配列を定義することもある。 この配列名と、そのあとに出てくる変数名がダブってはいけない。

int a[N];                       /* この配列はプログラム全体から“見える”*/

main() {
    int i, n;                   /* この i や n と… */

    ...
}


int power(int base, int n) {    /* この n や */
    int i, p;                   /* この i は別物。それぞれの関数の中から */
                                /* しか“見えない” */
    ...
}

(29.16) ※ return は、必要なら一つの関数の中に複数個おいてもよい。

    if (...) {
        return p;
    } else {
        return -p;
    }

(29.17) ※ 実用の上では、 ベキ乗の計算にはそなえつけの数学関数 pow() を使うのがよいだろう。 なお、 二乗や三乗の場合は pow(x,2), pow(x,3) よりも x*x, x*x*x と書いてしまうほうが速い。

(29.18) ※ 教科書 33 ページの「旧式の版」と書かれた書き方を覚える必要はまったくない。

(29.19) 次のプログラム片は、K&R2 §1.8 からの引用である。 (一部変更してある。)

int power(int base, int n) {        /* K&R2 より */
    int p;

    for (p = 1; n > 0; n--) {
        p = p * base;
    }
    return p;
}
前のプログラムの関数 power() の定義の部分だけを上で置き換えてもプログラムは同じように動く。

(29.20) この書き方では、 関数 power() の中で引数 n の値が変わる。 しかし、呼び出し側の ab の値は変わらない。 power(a, b) を計算するときに power() に渡した数は ab のコピーであって変数そのものではない、 というのがC言語の約束である。 このことを「call by value(値による呼び出し)」ということがある(らしい)。

§30 練習問題

(30.1) その0. (29.3) のプログラムの main() を変え、 その中で変数 i を使ってみよ。 例えば i を 0 から 9 まで動かしながら 2 と -3 の i 乗を計算させてみる、など。 この実験により、main() と自作関数とで同じ変数名を用いたとしてもそれらは別物であることがわかるだろう。

(30.2) その1. 上の int power(int base, int n) を改造し、 浮動小数点型の整数ベキ乗も計算できるようにした double power(double base, int n) を書け。 さらに、n が負の整数のときも正しく計算できるように改良せよ。

(30.3) ※ 単に「関数 power()」でなく 「関数 int power(int base, int n)」と呼ぶことで、 引数の数や型、返り値の型をも伝えることができる。

(30.4) ※ 「関数を作る」というのは、 「テキストファイルとして作成して終わり」ではなく、 検査用の main() も書いて、コンパイルして実行し、 思ったように動くことを確認することである。

(30.5) その2. 階乗を計算する関数 factorial() を書け。 引数や返り値の型は自分で考えること。 main() では 0 から適当な自然数までの階乗を計算させてみればよいだろう。

(30.6) ※ すぐにできない人はその前に

    int n, f;

    printf("いくつの階乗を計算しますか?\n");
    scanf("%d", &n);
        /* ここで n の階乗を計算する */
    printf("%d の階乗は %d です.\n", n, f);
として 「4 の階乗は 24 です.」が出力されるようなプログラムを書いてみよ。 それを参考にすればできるはずだ。

§31 発展問題

(31.1) その3. 0 以上の整数 n および 0 以上 n 以下の整数 r に対し、 n 個のものから r 個のものを取り出す方法の総数 nCr の値を返す関数 int comb(int n, int r) を書け。 テスト用の main() は各自で工夫せよ。 パスカルの三角形が書ければ美しかろう。 (階乗を返す関数を使うのが一つの方法だが、 すぐに桁あふれしてしまうのが欠点である。 この点を改良したものは書けるか?)

(31.2) その4. いままでのプログラムのうちのいくつかを、 「main() と自作関数」の形に分けて書いてみよ。 どれがそれに適しているか、 どんな関数にしたらよいかは、各自で考えること。

(31.3) その5. 初等超越関数(指数関数、対数関数、三角関数など) を自作してみよ。 Taylor 展開を利用して計算するので、忘れている人は調べておくこと。 「収束するまで足す」というのは(現段階では)むずかしいので、 決まった項数で打ち切ればよいだろう。 テスト用 main() ではもともとある関数と自作の関数を呼んだ結果を並べて出力するとよい。 (自作の exp や sin に expsin という名前をつけると、 もともとある関数の名前と重複してしまってまずい。 こんなときは myexp, mysin とするのも一案だ。 「私の exp」「私の sin」といった感じ。)

(31.4) その6. 次のプログラムは、関数 int swap(int x, int y) を書いて xy の値を入れかえようとしたものだが、 うまく働かない。なぜか?

#include <stdio.h>

int swap(int x, int y);

main() {
    int x, y;

    x = 1; y = 2;
    swap(x, y);
    printf("%d %d\n", x, y);
}


int swap(int x, int y) {
    int tmp;

    tmp = x; x = y; y = tmp;
}
なお、 今まで習った範囲の知識では二つの変数の値を入れかえる関数は書けない。 なぜか。少し考えてみよ。


岩瀬順一