2011 年度「計算数学」 2011-10-28

§21 多重ループ

(21.1) 前のセクションで出てきた二重ループを用いた自明なプログラム。 %2d は、 「一けたの数の場合、十の位は空白で埋めることによって二けたの幅で出力せよ」 の意味である。

#include <stdio.h>

main() {
    int i, j;

    for (i = 1; i < 10; i++) {
        for (j = i; j < 10; j++) {
            printf("%d * %d = %2d  ", i, j, i*j);
        }
        printf("\n");
    }
}

これは、掛け算九九の表のうち、乗数が被乗数以上のもののみを出力する。 昔は、8 * 3 は 3 * 8 に等しいから覚えなくてよいと考え、 「はっさんにじゅうし」は教わらなかったと聞く。その時代の表、ということになる。

(21.2) 練習問題:上のプログラムに少し手を加え、次のように出力されるようにしてみよ。

1 * 1 =  1  1 * 2 =  2  1 * 3 =  3  1 * 4 =  4  1 * 5 =  5  1 * 6 =  6  ...  1 * 9 =  9
            2 * 2 =  4  2 * 3 =  6  2 * 4 =  8  2 * 5 = 10  2 * 6 = 12  ...  2 * 9 = 18
                        3 * 3 =  9  3 * 4 = 12  3 * 5 = 15  3 * 6 = 18  ...  3 * 9 = 27
                                    4 * 4 = 16  4 * 5 = 20  4 * 6 = 24  ...  4 * 9 = 36
                                                5 * 5 = 25  5 * 6 = 30  ...  5 * 9 = 45
                                                            6 * 6 = 36  ...  6 * 9 = 54
                                                                        ...  7 * 9 = 63
                                                                        ...  8 * 9 = 72
                                                                        ...  9 * 9 = 81

§22 関数の自作

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

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

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

#include <stdio.h>

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

    printf("0 以上の整数を入力してください.\n");
    scanf("%d", &x);
    printf("0 以上の整数を入力してください.\n");
    scanf("%d", &y);

    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);
}

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

#include <stdio.h>

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

main() {
    int x, y;

    printf("0 以上の整数を入力してください.\n");
    scanf("%d", &x);
    printf("0 以上の整数を入力してください.\n");
    scanf("%d", &y);

    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;
}

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

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

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

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

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

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

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

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

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

(22.14) ユーザが 3 と 5 を代入したとする。 まず、 x が 3, y が 5 で power(x, y) を呼び出す。 呼び出された power() 側では base が 3, n が 5 で実行が始まる。 次に、 x, y の値はそのままで power(y, x) を呼び出す。 呼び出された power() 側では base が 5, n が 3 で実行が始まる。

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

#include <stdio.h>

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 が返る−−−−−−−−−−−−−−−−−−−−−−−−−−−+

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

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

#include <stdio.h>

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

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


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

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

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

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

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

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

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

前のプログラムの関数 power() の定義の部分を次で置き換えてもプログラムは同じように動く。

int power(int base, int n) {        /* 単独では動かない! */
    int p;

    for (p = 1; n > 0; n--) {
        p = p * base;
    }
    return p;
}

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

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

§23 練習問題

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

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

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

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

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

    int n, f;

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

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

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

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

(23.7) その3. 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 を返すものとする。

(23.8) その4. 0 以上の整数 x, y に対し、 それらの最大公約数を返す関数 int gcd(int x, int y) を書け。 同じ状況下で、最小公倍数を返す関数 int lcd(int x, int y) を書け。

付録:入出力リダイレクトとパイプ

前回の (20.1) のプログラムの結果をファイルに収めたいとする。 GNOME 端末から emacs へコピー & ペーストしてもよいのだが、 次のような方法がある。

画面に出力するプログラム a.out を、 結果をファイルに書き出すように動かすには、 「./a.out > ファイル名」とする。 同名のファイルがあると上書きされてしまう。 ファイルの末尾につけたすなら、 「./a.out >> ファイル名」。

キーボードから入力するプログラム a.out の入力を、 あるファイルの内容で代用するには、 「./a.out < ファイル名」。 以上を「入出力リダイレクト」という。

二つのプログラム a, b があって、 a の出力を b の入力としたいなら、 「./a | ./b」とする。 これを「パイプ」という。


岩瀬順一