2002 年度「計算機基礎論3B」 2002-11-22

練習問題(前回の補足)

0 で割り算をするプログラムを実行するとどうなるか、試しなさい。

ループ

C言語でくり返しを実行させる方法には三種類ありますが、 ここでは forwhile だけを説明し、 do-while はとりあげません。 K&R2 では §1.2, 1.3, 3.5 を見てください。 do-while は §3.6 にあります。

    for (文1; 式2; 文3) {
        ...
    }
は次のように動きます。

もう一つの構文

    while (式) {
        ...
    }
は次のように動きます。

forwhile も、中カッコの中(ループ本体)が一度も実行されない場合があります。

#include <stdio.h>  /* ユーザが数を入力すると,1 からその数までの和を出力 */

main()
{
    int i, n, sum;

    printf("いくつまで足しますか?\n");
    scanf("%d", &n);

    sum = 0;

    for (i = 1; i <= n; i++) {
        sum = sum + i;
    }
    printf("1 から %d まで足すと %d です.\n", n, sum);
}

i++ は「i を 1 だけ増やせ」、 i-- は「i を 1 だけ減らせ」の意味です。 それぞれ「i = i+1」「i = i-1」と書いても同じこと。 これらは等式としては正しくありませんが、 「ii+1 を代入せよ」 「ii-1 を代入せよ」の意味だからいいのです。 「sum = sum + i」も同様。 ++, -- については K&R2 §2.8 に述べられています。

上のプログラムで入力する数を 10, 100, 1000, 10000, ... のようにだんだん大きくしてゆくと、 どこかで int 型の限界を越えて答えがおかしくなります。 その場合でもエラーメッセージなどは一切出ません。 (やってみましょう。)

この例では for を使うほうが自然でしょうが、 次に while を使って書いてみました。

#include <stdio.h>  /* ユーザが数を入力すると,1 からその数までの和を出力 */

main()
{
    int i, n, sum;

    printf("いくつまで足しますか?\n");
    scanf("%d", &n);

    sum = 0;
    i = 1;

    while (i <= n) {
        sum = sum + i;
        i++;
    }
    printf("1 から %d まで足すと %d です.\n", n, sum);
}

次の例は while を使うほうが自然でしょう。

#include <stdio.h>  /* アラレ数 */

main()
{
    int n;

    printf("自然数を入れてください.\n");
    scanf("%d", &n);
    while (n != 1) {
        if (n % 2 == 0) {       /* n が偶数のとき */
            n = n / 2;
        } else {                /* n が奇数のとき */
            n = 3 * n + 1;
        }
        printf("%d ", n);
    }
}
アラレ(霰)数とは という漸化式で定義される数列です。 この数列は、初項がいかなる自然数であってもそのうち 1, 4, 2, 1, 4, 2, ... というループに落ち着くであろうと予想されているそうです。

このプログラムは、ユーザが入力した数から始めて 1 になるまで、 この数列を出力します。

※ ここで中カッコが入れ子になった場合のインデントのしかたを説明する。 上のプログラムの while の部分で確かめてみるとよい。

この方針を守っていれば、「ある中カッコに対応する中カッコをさがす」ことや 「ある文を囲む最小の中カッコをさがす」ことはきわめて容易である。 さらに、main() の直後の { についても同じ原理に従うことにし、
main() {
    ...
}
のように書いても構わない。

#include <stdio.h>  /* 自然対数の底 e の計算 */

main() {
    int n;
    double sum, kaizyo;

    sum = 1;
    kaizyo = 1;
    n = 1;

    for (n = 1; n < 24; n++) {
        kaizyo = kaizyo * n;
        sum = sum + 1 / kaizyo;
        printf("第 %d 項までの和は %.53f です.\n", n, sum);
    }
}

自然対数の底 e を e = 1/0! + 1/1! + 1/2! + 1/3! + ... として計算しています。 C言語には階乗を表わす演算子はないので自力で計算しなければなりません。 ここでは n! を計算した結果を捨てないで次の (n+1)! の計算に使っています。

なお、「kaizyo = kaizyo * n」のところでは double 型と int 型を掛けています。 こういった場合、結果は double 型になります。 C言語ではこのあたりが煩雑です。 K&R2 §2.7, A6 に出ていますが、 とりあえずは intdouble しか使わないので 「intdouble を混ぜたら double になる」 とだけ覚えておけばよいでしょう。

※ 岩波「数学辞典第3版」1434 ページによれば e = 2.7182818284590452353... であり、 上のプログラムの計算結果は途中で違っている。 コンピュータの計算は近似計算なので、 むやみに桁数を多く表示させても意味がないときもある。

「かつ」や「または」は次のように書きます。 これらは forwhile の条件部にも使えます。 K&R2 では §2.6 です。

    if ((x > 0) && (x < 8)) {       /* 「かつ」 */
        ........
    }

    if ((x < 0) || (x > 2)) {       /* 「または」 */
        ........
    }
なお、x > 0 などを囲んでいる小カッコは実は不要です。 これについての詳細は K&R2 §2.12 を参照してください。

※ 「かつ」も「または」も同じ文字を二つ打つことに注意。 まちがって「&」「|」をひとつだけ書いた場合、 それらにも意味があるのでコンパイラはエラーメッセージを出さない (かもしれない)。

二重ループ

次のプログラムは九九の表を出力します。 for を二重に使っていること以外は、 いままでに習ったことでわかると思います。

#include <stdio.h>

main()
{
    int i, j;

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

配列

「配列」というのは変数の列です。 K&R2 では §1.6 にあります。

#include <stdio.h>  /* エラトステネスのふるいで 10000 以下の素数を全て求める */

#define UNERASED 0              /* ←英語として正しいかどうか */
#define ERASED 1                /*   責任はもてません :-)     */

int a[10000+1];                         /* 配列 a[0], ... , a[10000] */

main() {
    int n, i;

    for (n = 2; n <= 10000; n++) {      /* 配列の初期化 */
        a[n] = UNERASED;
    }

    for (n = 2; n <= 100; n++) {        /* ふるいにかける */
        for (i = 2; n * i <= 10000; i++) {
            a[n*i] = ERASED;
        }
    }

    for (n = 2; n <= 10000; n++) {      /* 結果の出力 */
        if (a[n] == UNERASED) {
            printf("%d は素数.\n", n);
        } else {
            printf("%d は合成数.\n", n);
        }
    }
}

エラトステネスのふるい (Eratosthenes's sieve)を使い、2 以上 10000 以下の自然数について素数か合成数かを判定して出力するプログラムです。

数学科のみなさんには説明するまでもないでしょうが、 紙と鉛筆でやる場合はこうやります。 1 から 10000 までの自然数を書いておき、 まず 2 の倍数を斜線で消します。 次に 3 の倍数を消します。 次に 4 の倍数ですが、 これはすでに 2 の倍数として消されているので消さなくてよい……。 こうやって 100 の倍数まで消していって、 残ったものが素数でした。

int a[10000+1]; は 10001 個の int 型変数の配列 a[0], ... , a[10000] を宣言する構文です。 C言語の配列は必ず添字 0 から始まるので、 a[1] から a[10000] までを使うなら サイズ 10000 ではなくサイズ 10001 の配列が必要です。 int a[10001]; と書いてもいいのですが、 「a[0] があるから +1 が必要だよ」 ということを強調するためわざわざこう書きました。

そして、「斜線で消す」という代わりに、 a[5] に 0 が代入されていたら 5 は消されていない、 1 が代入されていたら消されている、としました。

#define で始まる二行ですが、 0 と 1 のどちらがどちらの印だったかわからなくなるといけないので、 こうやって名前をつけました。 #define にはこういう用法もあります。 この UNERASEDERASED は変数名ではなく定数名なので大文字で書いています。

三つの for 文が何をやっているかは、 簡単にコメントしておいたので読めばわかると思います。 なお、ここでは合成数の倍数も消していますので、 このプログラムは最も効率のよいものではありません。

※ C言語では、配列の要素 a[i] の値を調べたり a[i] に代入したりするとき、 添字 i が正しい範囲にあるかどうかチェックしない。 もしも上のプログラムで a[20000] に代入したとしても、 コンパイル時にも実行時にもエラーメッセージは出ない。 しかし、 プログラムが暴走したりおかしな結果が出たりすることは起こり得るので、 絶対にそのようなプログラムを書いてはならない。

プログラムの中止・一時停止・再開、more

以下の練習問題をやる前に、 エラトステネスのふるいのプログラムで次の練習をしておくとよいでしょう。

出力が大量になり、最初の部分がよく見えない場合などは、 「./a.out | more」とします。 一画面分だけ出力されると「--続ける--」が出て止まりますので、 スペースバーを押せば次に進みます。 次に進まないでプロンプトに戻りたい場合は「q」を押してください。

練習問題

全部やらなくても構いません。


岩瀬順一