2017 年度「計算数学a」 2017-11-01

§4.1 関数の自作

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

(4.1.2) ※ ここは,少し難しい。 その難しさは,初めて学ぶ西洋語で 「次の二つの文を,関係代名詞を使って一つの文にしてみましょう」 という練習問題に出会ったときに似ていると思う。 与えられた二つの文はいままで習ってきた知識で容易にわかるのに, どうやって一つの文にしてよいかわからなかった, という経験はないだろうか。 C言語では,逆に, 全体を一つの main() にしていたときはわかっていたのに, この部分を自作関数にして二つに分けよ,と言われると,とまどう人が多い。 関係代名詞を学んだときと同様に,ここは,慣れて乗り切るしかない。

(4.1.3) 次のプログラムは,xy 乗と yx 乗を計算して出力する。 いままでの知識で容易に理解できると思う。 p は答えがはいる変数, i はループを回った回数を覚えておく変数である。 1 に xy 回かけることで, xy 乗を求めている。 このやりかたでは,0 の 0 乗は 1 になる。

(4.1.4)

#include <stdio.h>

int main() {
    int x, y, i, p;

    x = 3; y = 5;       /* このように一行に書いてもよい */

    p = 1;
    for (i = 0; i < y; i++) {
        p = p * x;
    }
    printf("%d の %d 乗は %d です.\n", x, y, p);

    p = 1;
    for (i = 0; i < x; i++) {
        p = p * y;
    }
    printf("%d の %d 乗は %d です.\n", y, x, p);
}

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

#include <stdio.h>

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

int main() {
    int x, y;

    x = 3; y = 5;       /* このように一行に書いてもよい */

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

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

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

for (i = 1; i <= y; i++) ではなく for (i = 0; i < y; i++) とするのはC言語の習慣だと思う。 文字数が少なくて済む,という利点もある。

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

(4.1.7) 「以下,関数 power() の定義」以下を,順に説明してゆく。

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

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

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

(4.1.11) p = 1; とその次の for ループは(本質的には) 前と全く同じである。 basen 乗を計算し, 最後の「return p;」でその値を返す。 return のうしろに書いた式の値がこの関数の値になる, という約束である。

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

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

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

(4.1.15) つまり,まず, x が 3, y が 5 で power(x, y) を呼び出す。 呼び出された power() 側では base が 3, n が 5 で実行が始まる。 次に, x, y の値はそのままで power(y, x) を呼び出す。 呼び出された power() 側では base が 5, n が 3 で実行が始まる。

(4.1.16) さらに細かく説明しよう。。

#include <stdio.h>      /* これは説明のためのものです。コンパイルできません */

int main() {
        ...                            3  5        3  5
    printf("%d の %d 乗は %d です.\n", x, y, power(x, y));
        ...                                  ~~~~~~~~~~~
}                                                | ↑
                   +−−−ここへとぶ−−−−−−+ | 
                   ↓                               +−243 が出力される−+
               3        5                                                 |
int power(int base, int n) {    /* 以下,関数 power() の定義 */           |
    int i, p;                                                             |
                                                                          |
    p = 1;                                                                |
    for (i = 0; i < n; i++) {                                             |
        p = p * base;                                                     |
    }                                                                     |
    return p;                                                             |
}         243 が返る−−−−−−−−−−−−−−−−−−−−−−−−−−−+

x が 3, y が 5 で main() 内の power(x,y) にさしかかったとする。 すると,power() へとび, base が 3, n が 5 で動き始める。 power() の終わりにある return p; にさしかかったとき, p の値は 243 である。よって, main() 内の power(x, y) が 243 に置き換わって出力される。

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

(4.1.18) 言い方を変えれば,main() の中だけで使う変数は main() の中カッコの中で定義する。 その他の自作関数の中だけで使う変数はその中カッコの中で定義する。 これらは名前がダブっていても別物である。 それとは別に,(この授業では)main() の前で変数や配列 (変数の列。後日,述べる)を定義することもある。 この変数名・配列名と,関数の中でだけで使う変数名がダブるのはうまくない。

#include <stdio.h>

int a[N];                       /* この配列(変数の列,後日,述べる)と */
int count;                      /* この変数はプログラム全体から“見える”*/

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


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

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

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

また,return f(x)*y; とか,return 0; などと書いてもよい。

(4.1.20) ※ 二乗や三乗の場合は,関数を呼び出すより x*x, x*x*x のほうが速い。

(4.1.21) 自作関数の書き方を,もう一度,まとめてみよう。 上の例では,power(x, y) と呼び出されたときの x, y の値を, 自作関数の側では base, n として受け取る。 これらと,ほかにその自作関数の中で宣言した変数 (上の例では ip)を使って計算し, 答えを return すればよい。

(4.1.22) なお,C言語では,関数が受け取った base, n の値は, 関数の中で必要なら変えてしまっても構わない,という規則になっている。 (これはたぶんこの授業では使わない。)

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

§4.2 練習問題

(4.2.1) 階乗を計算する関数 int factorial(int n) を書け。 main() では 0 から適当な自然数までの階乗を計算させてみればよいだろう。

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

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

※ 次のような, 「4 の階乗は 24 です.」が出力されるプログラムが参考になる。

    int n, f;

    printf("いくつの階乗を計算しますか?\n");
    scanf("%d", &n);

    ...                 /* この部分で n の階乗を計算する */

    printf("%d の階乗は %d です.\n", n, f);

次に,double factorial(int n) に改造せよ。

(4.2.2) double 型変数 x と自然数 n に対し, xn 乗を返す関数 double power(double x, int n) を書け。

(4.2.3) 0 以上の整数 n および 0 以上 n 以下の整数 r に対し, n 個のものから r 個のものを取り出す方法の総数 nCr の値を返す関数 int comb(int n, int r) を書け。 テスト用の main() は各自で工夫せよ。 パスカルの三角形が書ければ美しかろう。

階乗を返す関数を使うのが一つの方法だが, すぐに桁あふれしてしまうのが欠点である。 階乗を double 型で計算するようにしたらどうか?

そのほかの工夫はあるか?

(4.2.4) xy 未満なら負の数, xy が等しければ 0, xy より大きければ正の数,を返す関数 int comp(int x, int y) を書け。

§4.3 発展問題

(4.3.1) 2 以上の整数 n と, 1 以上 n 未満の整数 x に対し, x の mod n での逆数を返す関数を書け。 すなわち, x * y と 1 とが mod n で等しくなるような, すなわち, x * yn で割った余りが 1 になるような y の値を返す関数 int inv(int x, int n) を書け。 なお,そのような y が存在しないときは 0 を返すものとする。

(4.3.2) 整数 x, y に対し, それらの最大公約数を返す関数 int gcd(int x, int y) を書け。 x, y の動く範囲 --- 数学ならば定義域 --- は各自で考えて決めよ。 また,最小公倍数を返す関数 int lcm(int x, int y) を書け。

(4.3.3) 自然数 x に対し, その最小の素因数を返す関数 int prim(int x) を書け。 その関数を利用して,素因数分解を行なうプログラムを書け。 すなわち,たとえば 24 を入力すると 「24 = 2 * 2 * 2 * 3」と出力するプログラムを書け。

(4.3.4) Möbius 関数,Euler 関数の定義をインターネットで調べ, 自作せよ。

(4.3.5) 自作関数を試す際,乱数(後日,述べる)を用いる方法がある。 たとえば,最大公約数では,1 から 100 までの整数を二つ, 乱数で決め,それらの最大公約数を出力させるのである。 (引数の範囲を限るのは,あまりに大きいと結果が正しいかどうか確かめづらいからである。)

上の練習問題のうちのいくつかを,そのように改変してみよ。


岩瀬順一