繰り返し(ループ loop)

「♪くりかえす このポリリズム」

繰り返しとは

世の中には同じ事を何度も繰り返すことが多々ある。ちょっと考えただけでもたくさん思い浮かぶだろう。曲を例にとると、1番、2番、3番……と同じメロディになっていることがよくある。曲によってはまったく同じメロディで繰り返すものもあるし、間奏がが入ったり少々アレンジの入るものもある。

楽譜ではこのような繰り返しを反復記号を使って書く。例えばこのページで紹介されているように。反復記号を使うことで、同じメロディを何度も書く手間を省くことができるし、曲の流れ(構造)も理解しやすくなる。

プログラムでも同様である。

繰り返し=実行箇所の逆戻し

プログラムは上から下に、順に実行されると説明した(順次実行)。前回までに説明した条件分岐では、条件に応じて処理を飛ばすことができるが、順次実行の原則は変わらない。

では、実行している行を既に実行した行に戻すとどうなるか?

次のプログラムはその例である。goto loop;は実行する行をloop:に飛ばすことを意味する。

注意!! goto文はプログラムの流れを読みにくくする危険が高いため、一部の例外を除いて使うべきではない。どんな複雑な繰り返しでも、次に説明するwhile文あるいはfor文(およびbreak文とcontine文)だけ使って書くことができる。説明のためにここではgoto文を使うが、存在自体を忘れてしまっても構わない。

#include <stdio.h>
#include <stdlib.h>

int main() {
loop:
    printf("Hello World!\n");
    goto loop;

    exit(0);  // ここには到達しない
}

この処理の流れをフローチャートで描くと次のようになる:

処理を順に追っていくと、printf文でHello World!と表示し、loop:と書かれたところに戻る。すると次にまたprintf文でHello World!と表示し、loop:と書かれたところに戻り、またprintf文で……(以下無限に繰り返す)

注意!! このプログラムは無限に同じ場所を繰り返すのでプログラムが終了しない(無限ループと呼ぶ)。無限ループに陥ったプログラムを終了させるには、control + c で強制的に中断させる必要がある。

while文

繰り返しを記述するための基本的な文法がwhile文である。while文は次のように書く:

while (条件式) {
    実行文1;
    実行文2;
    実行文3;
    .....
}

実行文が1つしかない場合は
while (条件式) 実行文;
のように{ }を使わずに書くことができるが、if文と同様に { } を常に書く方が間違いが少ないし、実際的には実行文が1つしかないwhile文はあまり使われない。

条件式は、繰り返しを継続するか終了するかの判定に使われる。条件式が成り立っている場合(条件式の値が真の場合)は{ }の中に処理を進める。条件式が成り立っていない場合(条件式の値が偽の場合)は{ }の中は飛ばし、} の次の行に処理を進める。{ }の中の処理を進め、}に到達したらwhile文に処理を戻し、再び条件式で判定する。

フローチャートで描くと次のようになる:

最初から条件が成立していない場合は、繰り返し(while文の{ }の中身)は1回も実行されないことに注意。

while文による繰り返しが始まって終了するには、最初は条件式が成り立っていて、繰り返していくうちに条件式が成り立たなくなる必要がある。

例えば次のようなwhile文は条件式が常に成り立つので無限ループになる:

while (1 == 1) {  // 常に真
    printf("Hello World!\n");
}

while文の条件式が成り立たなくするためには、条件式で使っている変数の値が繰り返しの処理によって変化しなければならない。

これはwhile文の条件式で繰り返しを終了させる場合の話である。break文を使って繰り返しを抜け出す方法ではこの限りではない。

繰り返し回数を数えるwhileループ

例えば、Hello World!を5回だけ繰り返して表示することにする。紙にペンで書く際、普通は回数を数えるだろう。ではプログラムで回数を数えるにはどのようにしたらよいか?そこで、次のように考えてみる:

回数を数えるための変数numberを用意し、最初は0にしておく。1回"Hello World!"を表示するたびにnumberの値を1増やす。numberの値が5になったら繰り返しを終了する。

while文の条件式は成り立っている間は繰り返す、であるから、「numberの値が5になったら終了」というのを「numberの値が5未満なら続ける」と言い換える必要がある。つまりこうなる:

回数を数えるための変数numberを用意し、最初は0にしておく。1回"Hello World!"を表示するたびにnumberの値を1増やす。numberの値が5未満なら繰り返しを続ける。

「numberの値が5になったら終了」をそのまま裏返すと「numberの値が5でなければ繰り返す」となる。この条件でも間違いではないが、繰り返しの意味的には「numbeの値が5になるまで繰り返す」に相当する「numberの値が5未満なら繰り返す」の方が良い。

この処理の流れをフローチャートで描くと次のようになる:

「numberの値を0にする」には、numberに0を代入すればよい。

0を代入しなければならないことに注意!! C言語では変数を宣言した時点ではどのような値になっているかは決まっていない。このため、何かの値を代入する必要がある(初期化と呼ぶ)。なお、変数宣言と初期化は次のように同時に行うことができる:

int number = 0;

「numberの値を1増やす」には、numberと1を足した値をnumberに代入すればよい。つまり、

number = number + 1;

これは次のようにも書けることは既に説明したとおり:

number += 1;
number++;

練習:"Hello World!"を5行表示するプログラムを完成させよ。

このnumberのような、繰り返し回数を数えるための変数をカウンタ変数と呼ぶ。繰り返しの間にカウンタ変数の値がどう変化するかを確認してみよう。

練習:"Hello World!"の代わりに、"Hello World! 1回目"のように、何回目かを表示するようにプログラムを修正せよ。

do-while文

条件分岐の回の宝箱の例題で1〜3の番号を入力させるプログラムを作成したが、1〜3以外でも入力できてしまっていた。では、入力された値が1〜3でなかったらもう一度入力させるようにしてみよう。

簡単のため最初のプログラムで説明する(今回説明する部分はどの変化形のプログラムでも同じようになるので、各自で適当なプログラムを修正すること)

/*****
    treasure.c
    宝箱を開けてみる
    M.Minakuchi
*****/

#include <stdio.h>
#include <stdlib.h>

int main() {
    int box;  // 選んだ宝箱の番号

    printf("宝箱が3つある!どれを開けますか?(1か2か3): ");
    scanf("%d", &box);

    if (box == 1)  printf("宝箱は罠だった\n");

    exit(0);
}

scanf文で入力された値が1〜3でなかったら1つ前のprintf文に戻すことにする。while文を使うとこうなる(プログラムの一部のみ掲載):

    while (【boxの値が1〜3でなかったら】) {
        printf("宝箱が3つある!どれを開けますか?(1か2か3): ");
        scanf("%d", &box);
    }

練習:上のプログラムの断片中の、条件式の部分をプログラムで書くとどうなるか考えてみよう。

ただし、このままでは不十分である。boxの値は、変数を宣言しただけでは値が不定(どのような値になっているか決まっていない)なので、運悪くboxの値が1〜3であればwhile文の中の処理は飛ばされてしまう。これを防ぐには、確実にwhile文の条件が成り立っている値、例えば0を初期値として代入しておけばよい。

    box = 0;
    while (【boxの値が1〜3でなかったら】) {
        printf("宝箱が3つある!どれを開けますか?(1か2か3): ");
        scanf("%d", &box);
    }

もう一つの方法として、while文の代わりにdo-while文を使う方法がある。do-while文の書き方は次のようになる:

do {
    実行文1;
    実行文2;
    実行文3;
    .....
} while (条件式);

whileの条件式の後ろに;が必要なことに注意!!

do-while文の処理の流れをフローチャートで描くと次のようになる:

while文と違って、条件にかかわらず処理({ }の中身)を1回は実行する。

練習:do-whileを使って書き直してみよう。

do-while文を使わなくても、while文で変数の値を適切に初期化するなどの工夫をすることによって同じ処理を書くことは可能である。が、do-while文を使う方がプログラムがすっきりしたり、意図を明確に表現することができる場合もある。必要に応じて使うとよいだろう。

while文の応用

例題1. 夢想花

次の歌詞は夢想花(円広志作詞)の一節である。これを繰り返しを使って表示するプログラムmusoubana.cを作成せよ。

とんでとんでとんでとんでとんでとんでとんでとんでとんで
まわってまわってまわってまわる

【実行例】
$ gcc -o musoubana musoubana.c
$ ./musoubana
とんでとんでとんでとんでとんでとんでとんでとんでとんで
まわってまわってまわってまわる
$

ヒント:「とんで」の部分と「まわって」の部分は別のループにする。

何も表示せずに改行だけしたい場合、

printf("\n");

とすればよい。

例題2. 2乗の計算

1から入力された値までの2乗をそれぞれ計算して表示するプログラムsquare.cを作成せよ。
なお、入力された値が0より小さい場合は何も表示しなくてよい(入力のエラーチェックは不要)。

【実行例(下線は入力値の例、入力メッセージはこの通りでなくてもよい)】
$ gcc -o square square.c
$ ./square
input number: 7
1 * 1 = 1
2 * 2 = 4
3 * 3 = 9
4 * 4 = 16
5 * 5 = 25
6 * 6 = 36
7 * 7 = 49
$ ./square
input number: -3
$

ヒント:教科書 例5.3。2乗の計算式を表示するためのprintf関数の書き方(書式指定子と変数をどのように書かなければならないか)を考えよう。

例題3. 1からnまでの総和

1から入力された値までの総和を、繰り返しを使って求めるプログラムsum.cを作成せよ。
なお、入力された値が0より小さい場合は再度入力させるようにせよ。

【実行例(下線は入力値の例、入力・出力メッセージはこの通りでなくてもよい)】
$ gcc -o sum sum.c
$ ./sum
input number: 7
sum = 28
$ ./sum
input number: -3
input number: 14
sum = 105
$

ヒント:総和を求めるための変数を用意する。入力をやり直させるプログラム例はこの資料で既に解説している。

例題4. nの階乗

入力された値の階乗を求めるプログラムfactorial.cを作成せよ。nの階乗は、1からnの整数の積である。入力された値が0より小さい場合は再度入力させるようにせよ。
また、大きな値(17以上)を入力すると表示される結果がおかしくなるが、理由を考察せよ。

【実行例(下線は入力値の例、入力・出力メッセージはこの通りでなくてもよい)】
$ gcc -o factorial factorial.c
$ ./factorial
input number: 5
5! = 120
$ ./factorial
input number: -3
input number: 10
10! = 3628800
$

課題1.

漸化式 , で表される数列を入力された値の個数分表示するプログラムrecurrence.cを作成せよ。入力された値が0より小さい場合は再度入力させるようにせよ。

【実行例(下線は入力値の例、入力メッセージはこの通りでなくてもよい)】
$ gcc -o reccurence reccurence.c
$ ./reccurence
input number: 7
1
5
17
53
161
485
1457
$

ヒント:値を表示する処理と次の値を計算する処理の順序を考えてみよう(つまり、最初の項の表示をどうするか)。順序によって若干異なるプログラムとなる。

提出期限:6月23日(月) 13:15

for文

繰り返し処理でよく使われるパタンの1つに、カウンタ変数を使って繰り返し回数を数える、というものがある。while文を使って書くと次のようなパタンである。

int count;  // カウンタ変数

count = 0;  // カウンタ変数の値を0にする
while (count < 10) {  // countが10になるまで繰り返す
    // 繰り返し処理(具体的なプログラムは省略)
    count++;  // countの値を1増やす
}

このプログラムは何回繰り返されるか?countの値はどのように変化するか?

この繰り返しのために次の3つの部分がある:

【繰り返しを始める前に行う処理】
while (【繰り返しを続ける条件】) {
    // 繰り返し処理
    【1回繰り返すごとに行う処理】
}

繰り返し処理のところに書かれる処理が少ないプログラムでは問題がないが、ちょっと複雑なプログラムになると、どこがこれら3つの部分に相当するかが分かりにくくなる。そこで、C言語にはfor文という書き方が用意されている。for文は次のような文法になる:

for (【繰り返しを始める前に行う処理】; 【繰り返しを続ける条件】; 【1回繰り返すごとに行う処理】) {
    // 繰り返し処理
}

練習:上のwhile文で書かれたプログラムの断片をfor文を使って書き換えてみよう。

なお、for文のそれぞれの部分は必要がなければ省略可能である。また、複数の処理を書きたい場合は,(カンマ)で区切って書くこともできる(真ん中の繰り返し条件はカンマで区切る意味はない)。しかし、少々高度な書き方になるので慣れるまでは使わない方がよいだろう。

for文の応用

for文はカウンタ変数を使って繰り返し回数を数える書き方がよく使われるが、これに限っているわけではない。簡単な例で考えてみよう。

例題5. カウントダウン

例題:入力された値から1まで1ずつ減らして表示するプログラムcountdown.cを作成せよ。

【実行例(下線は入力値の例、入力メッセージはこの通りでなくてもよい)】
$ gcc -o countdown countdown.c
$ ./countdown
input number: 5
5
4
3
2
1
$

ヒント:カウンタ変数を1ずつ減らすようにすればよい。カウンタ変数を用意する代わりに、入力値を格納した変数の値を変化させてもよい。

例題6. 奇数のみ数える

例題:1以上100未満の奇数の2乗を表示するプログラムoddSquare.cを作成せよ。

繰り返し回数を数える考え方で処理の流れを書くと例えば次のようになる。1以上100未満の奇数は50個あることに注意。

練習:このフローチャートを元にoddSquare.cを作成せよ。

考え方としては正しいが、奇数の個数を数えるのはちょっと素直でない。1, 3, 5, 7, 9, ....と奇数のみを数えて、その2乗の値を表示する方が自然だろう。この考え方の処理の流れを書くと次のようになる。奇数は2ずつ増えていくことに注意。

練習:このフローチャートを元にoddSquare.cを書き換えよ。

これらはどちらが正しいというものではない。プログラムを書いた人の考え方次第である。ただし、通常はできるだけ理解しやすい書き方を選ぶのがよい。

余裕があれば、それぞれwhile文で書いてみよう。

2重ループ

繰り返しの中に繰り返しを入れることもできる。このような構造を2重ループ、あるいは入れ子構造、ネストnestなどと呼ぶ。

具体例を見てみよう(教科書p.90 プログラム例5.8を元に修正)

/*****
  doubleLoop.c
  繰り返しの入れ子の例
  M.Minakuchi
*****/
#include <stdio.h>
#include <stdlib.h>

int main() {
  int inner, outer;  // カウンタ変数

  for (outer = 0; outer < 3; outer++) {
    printf("before inner loop\n");
    for (inner = 0; inner < 3; inner++) {
      printf("outer = %d, inner = %d\n", outer, inner);
    }
    printf("after inner loop\n");
  }

  exit(0);
}

ダウンロードはこちら doubleLoop.c

コンパイル・実行して動作を確認しよう。

内側の繰り返しが一通り終わってから、外側の繰り返しが進む、というのがポイント。

内側と外側で別々のカウンタ変数を使っていることにも注意。同じカウンタ変数を使ってしまうと、思った通りに繰り返してくれなくなる。

doubleLoop.cで、内側のforループのカウンタ変数をinnerからouterに変更したらどうなるか。まず予想してみてから、プログラムを修正して動作を確認してみよう。

カウンタ変数の値を不用意に変更するのも間違いの元になる。例えば次のように1行書き加えてみよう。

/*****
  doubleLoop.c
  繰り返しの入れ子の例
*****/
#include <stdio.h>
#include <stdlib.h>

int main() {
  int inner, outer;  // カウンタ変数

  for (outer = 0; outer < 3; outer++) {
    printf("before inner loop\n");
    for (inner = 0; inner < 3; inner++) {
      printf("outer = %d, inner = %d\n", outer, inner);
      outer = 0;  // 追加
    }
    printf("after inner loop\n");
  }

  exit(0);
}

どうなるか予想してみよう。

このような簡単なプログラムなら気をつけていれば間違うことはないが、複雑になるとうっかり代入してしまうことがあるので気をつけよう。そのためには、カウンタ変数と分かる変数名を使うのがよい。通常は変数名は意味の分かる名前を使うが、慣習的にカウンタ変数はi, j, k...といった一文字のアルファベットがよく使われる。普通の変数に1文字の変数名を使うことを避ければ、一文字の変数は特殊であると区別しやすくなる。

i以降のアルファベットがカウンタ変数によく使われる理由の一つは、FORTRANという古いプログラミング言語では整数型はI〜Nのアルファベットで始まる名前の変数と決められていたからである。また、数学でもi, jが添字としてよく使われている。

次に扱う「配列」は繰り返しで使われることが多く、その際にカウンタ変数が長い名前だとかえってプログラムの可読性が落ちることがある。

意図的にカウンタ変数の値を変更して繰り返しの流れを変えることも可能であるが、プログラムの流れが理解しにくくなり間違いの元になりやすい。

ここでは単純な2重ループの例を紹介したが、3重以上の多重ループや、外側のループの中に複数のループが入っている構造など、必要に応じて自由に組み合わせることが可能である。

break文とcontinue文

どのような繰り返しパタンもwhile文(およびif文)のみで書くことが可能であるが、複雑になりすぎる場合もある。break文とcontinue文を使うことで簡単に書けることがある。これらは、何かの条件が成立したときに繰り返しの流れを変化させるために使う。

やや難しくなるので、ここでは簡単に使い方を説明するにとどめておく。まずはwhile文とfor文の書き方をしっかり習得して欲しい。

break文

break文は、繰り返しを中断してループの外側にジャンプする。

switch-case文で使うbreak文も意味的には同じである。

例えば次のコードは入力値が正の整数値であれば繰り返しを終了する。for (;;)は繰り返しの終了条件が書かれていない=常に終了しない無限ループとなっていることに注意。

無限ループを意図的に作成するには、while(1) などのwhileを使った書き方もあるが for (;;) の方が優れているとされる。

int number;

for (;;) {
    printf("input number: ");
    scanf("%d", &number);
    if (number > 0)  break;
    printf("input again\n");
}

printf("your number is %d\n", number);

break;により繰り返しを抜け出すと } の次の実行文に処理が飛ぶ。このプログラムの処理をフローチャートで描くと次のようになる:

continue文

continue文は、繰り返しの残りの部分を飛ばして、ループの先頭に戻り次の繰り返しを開始する。

例えば次のコードは1から100まで繰り返す間で、奇数であれば何も行わず、偶数であれば2乗を表示する。

int num;

for (num = 1; num <= 100; num++) {
    if (num % 2 == 1)  continue;
    printf("%d * %d = %d\n", num, num, num * num);
}

繰り返しの残りの部分を実行しないことに注意。例えば、上のコードをwhile文を使って次のように書き換えるとうまくいかない。

int num;

num = 1;
while (num <= 100) {
    if (num % 2 == 1)  continue;
    printf("%d * %d = %d\n", num, num, num * num);
    num++;
}

この場合はcontinue;で飛ばされないように、numの値を1増やす処理を先に実行するように変えればよい。

int num;

num = 0;
while (num < 100) {
    num++;
    if (num % 2 == 1)  continue;
    printf("%d * %d = %d\n", num, num, num * num);
}

numの初期値とwhile文の条件式が変化していることに注意。このようなコードの書き方はパタンが数多くあり、経験を積めば自然と習得していける。初心者の間は自分で考えて試してみることと、沢山のプログラム例を見て真似てみるのがよいだろう。

練習問題

例題7. 同心円

大きさ600 x 600のウィンドウに、中心座標が(300, 300)、半径が50, 100, 150, 200, 250の同心円を5つ、繰り返しを使って描くプログラムconcentric.cを作成せよ。

ヒント:カウンタ変数で回数を数えて繰り返す方法と、半径の値を変えながら繰り返す方法とが考えられる。どちらの方法でもよい。カウンタ変数で数える方法の場合は繰り返しの中で、描こうとしている円の半径を計算する必要がある。

例題8. 色つき同心円

例題7のプログラムを、内側から数えて奇数個目の円は青、偶数個目の円は赤で描くように修正したプログラムconcentricColor.cを作成せよ。

ヒント:円を描く直前で青か赤の条件を判定して色を設定する。どちらの色で描くかを決めるための変数を追加する方法もある。

例題9. マス目

整数値を入力させ、大きさ600 x 600のウィンドウに入力された数の行と列のマス目を描くプログラムgrid.cを作成せよ。入力値のエラーチェックはなくてもよい。また、ウィンドウの端は座標値によっては描いた線が見えないことがあるが構わない。

(入力値が9の例)

ヒント:まず入力された値からマス目の大きさを計算する。繰り返し回数を数える方法と、座標値を変えながら繰り返す方法とが考えられる(同心円の例題と同様)。

課題2.

整数値を入力させ、大きさ600 x 600のウィンドウに、1段目は1個、2段目は2個、3段目は3個……と繰り返して入力された値の数だけ繰り返して円を描くプログラムpyramid.cを作成せよ。入力値のエラーチェックはなくてもよい。

(入力値が12の例)

ヒント:2重ループを使う。次のフローチャートを参考にせよ(これ以外の方法で描いても構わない)。HandyGraphicでは左下が原点であることに注意(円の中心座標の計算がちょっとだけややこしいです)。

(6/18 フローチャート貼り忘れてました)

提出期限:6/26(木) 24:00