Cygwinでデバッグ

ループの書き方の習慣

最初に考えるべきこと

繰返し処理をしようとする時、当然 for ループや while ループを書くことになりますが、最初に考えることは何でしょうか。

そうです、ループの制御をするループ変数を何にするかを考えます。当然ですね。

ループ変数は、ループ処理中に頻繁に出現するので短い名前にします。通常は i, j あたりを使います。k は使っても構いませんが、l(エル)は数字の1と混同するので避けます。

文字列に関する処理だと s, t、ポインタに関係する処理だと p, q、座標に関係すれば x, y, z も使います。n や m は、ループ変数よりも、ループをどこまで回すかの上限値に使われることが多いように思います。

ループ変数が足りなくなってきたら、それは関数の設計に問題があります。関数をもっと小さく分割すれば、ループ変数が足りなくなることはないはずです。1つの関数の大きさの目安は30行以内とも、30秒以内に理解できる範囲とも言われています。

良いループ・悪いループ

さて、10回ループの書き方は、もちろん何通りもあるのですが、良いものと悪いものがあります。

○(1) for ( i=0; i<10;  i++ ) { ... }
○(2) for ( i=1; i<=10; i++ ) { ... }
×(3) for ( i=0; i<=9;  i++ ) { ... }
×(4) for ( i=1; i<11;  i++ ) { ... }

×のついているものは、なぜ悪いのか、一目瞭然ですね。確かに10回のループはするのですが、プログラム上には「10」という数字が表れていないからです。

ここからわかることは、次のようにまとめられます。

つまり、もし(n-1)回のループをさせたいなら、(n-1)という表記になるようにすべきであって、n に = をつけたり取ったりして調節すべきでない、ということでもあります。(n-1)回ループの例を挙げます。

○(1) for ( i=0; i<n-1;  i++ ) { ... }
○(2) for ( i=1; i<=n-1; i++ ) { ... }
×(4) for ( i=1; i<n;    i++ ) { ... }

(1)(2) のどちらを使うかは、場面によって判断します。C言語の配列の添え字は 0 から始まりますので、配列を扱うなら (1) が向いていることが多いでしょう。日常生活で数を数えると 1 から始めますから、それに関係する場合は (2) がよいでしょう。

常套句との類似性

次の2つのプログラムは、どちらも同じ働きをしますが、どちらが読み易いでしょうか。

プログラム[A]
for ( i=0; i<10; i++ ) {
    a[i] = -i;
}
プログラム[B]
for ( i=0; i>-10; i-- ) {
    a[-i] = i;
}

どちらも a[0] = 0, a[1] = -1, ..., a[9] = -9 のように代入を行いますが、多くの人はプログラム[A]のほうが理解しやすいでしょう。それはなぜでしょうか。

配列 a[] を 0 で初期化する処理といえば、次の処理を思い浮かべる人が多いでしょう。

for ( i=0; i<10; i++ ) {
    a[i] = 0;
}

おそらく、この常套句に似ている方が読み易いのでしょう。ループ変数の値が減るよりも増えるほうに慣れているのは当然です。また、配列の添字がループ変数になっていると安心である、ということも言えそうです。プログラム[B]では、たまたま -i が添字になってくれましたが、このような状況はめったにないことです。やはり、よくある処理と似たプログラムのほうが理解しやすいですし、処理に変更があった時にも対処しやすいです。

ループ変数の役割

最後に、ループ変数の扱いについて触れておきます。ループ処理中にループ変数を加工しようとする人がいますが、たいていはうまく行かないので、別の方法を考えることを提案しておきます。

プログラム[A]
for ( i=0; i<10; i++ ) {
    if ( i%2 == 0 ) i++;  // 偶数なら奇数にする
    printf("%d\n", i);
}

プログラム[A]が奇数を5個表示すると言われても、(コメントがなければ)多くの人は面食らうでしょう。

プログラム[B]
for ( i=0; i<10; i+=2 ) {
    printf("%d\n", i+1);
}
プログラム[C]
for ( i=0; i<10/2; i++ ) {
    printf("%d\n", i*2+1);
}

プログラム[B][C]ならば素直で、コメントがなくても理解できるでしょう。

次のプログラムは、そもそもループ変数がどれだかよくわからないので、何回ループするのか見当がつきません。

for ( i=0; j<10; k++ ) {
    if (i==0) k=0;
    j=i;  i=k;
    printf("%d\n", j);
}

さて、ループ変数が2個あるように見えるのも、あまりよい気がしません。 例えば次のプログラムは、入門書に例としてよく載せられるものですが、for ループに多くを詰め込みすぎている気がします。

void string_copy(char *dst, char *src) {
    char *d, *s;

    for ( d=dst,s=src; *s!='\0'; d++,s++ ) {
          *d = *s;
    }
    *d = '\0';
}

よく見ると、終了条件に関係するのは s だけですから、これが本当のループ変数です。それならば、はじめから s がループ変数であると分かるように書けばどうでしょうか。例えば次のようになります。

void string_copy(char *dst, char *src) {
    char *d, *s;

    d = dst; s = src;
    while ( *s != '\0' ) {
          *d = *s;
          d++; s++;
    }
    *d = '\0';
}

あるいは、ポインタをやめて、配列でアクセスすることにすれば、変数は1つで十分です。(もっとも、実行効率が下がる可能性はあります。)

void string_copy(char *dst, char *src) {
    int i;

    for ( i=0; src[i]!='\0'; i++ ) {
          dst[i] = src[i];
    }
    dst[i] = '\0';
}

このように、ループ変数の役割は大切です。ループ変数には、ループ1回ごとに規則正しく変化させたい変数を選び、終了条件を見れば、いつループが終わるのかが想像できるようにします。

そしてループ処理には、ループ変数が主役となって登場します。ループ変数が現れないとすると、何のためのループかわからなくなってしまいます。

「とりあえず無限ループを書いて、適当な条件で break する」というのは悪い癖で、プログラマから「何を主体にループを回すのか」という視点が抜けてしまい、目的の不明瞭なループを書いてしまいがちになります。無限ループを使うのは、終了条件が素直に書けない場合の最後の手段です。そして、「いつからいつまでループするのか、わかりにくくてゴメンなさい、ループ処理をよく読んで下さい」と謝りながら使うものです。

以下はキーボードから1文字ずつ受け取る常套句ですが、なぜ条件が副作用を伴う複雑な式になっているかと言えば、この条件式を分割すると無限ループが必要になるからです。

while ((c=getc()) != EOF) { ... }  ファイルの終わりまで1文字ずつ受け取る常套句

                                   同じ処理の条件式分割版
while ( 0 == 0 ) {                 ループ1回ごとに変化する変数はどれ?
    c = getc();                    ループの終了はいつ?
    if (c == EOF) break;
    ...
}

常套句ですから、このような複雑な条件式も許されます。しかし、もし常套句でなければ、条件自体は分割したほうが理解しやすそうです。もっとも、分割するとループの主体がわかりにくくなるので、こちらにも問題があります。悩ましい所です。


トップ   編集 凍結 差分 履歴 添付 複製 名前変更 リロード   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2020-11-10 (火) 11:58:32