制御構造2(反復)

さまざまなデータ処理プログラムを作りながら, 反復制御(for,while,do-while)について理解しよう.

教科書の該当範囲:第3.3節の前半(3.3.1〜3.3.3)

基礎知識

反復の分類
for 文:計数反復

ソースコード記述方法:

int カウンタ変数;		// 反復回数を入れるよ
for (カウンタの初期化; 反復条件; カウント) {
	繰り返したい処理
}

例1:カウントアップ(0 から 9 まで数え上げる)

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

動作:

なぜ,9 までと言いつつ,10 を使うのか?0 から 9 までの 10 回なので. (もちろん,条件式を i <= 9 としてもよい.)
なお,i++i = i + 1 の短縮版ね. (実は微妙に違いますが,今は気にしないでおく.)
そして,このループの終了後,カウンタ i の値は 9 ではなく, 10 になっていることに注意しよう.

例2:カウントダウン(9 から 0 まで数え下げる)

int	i;
for (i = 9; i >= 0; i--) {
	printf("%d\n", i);
}
ここで,カウントダウン(count down,またはデクリメント decrement)i-- は, i = i - 1 の短縮版ね.(実は微妙に...同上.)
while 文:前判定条件反復

ソースコード記述方法:

while (反復条件) {
	繰り返したい処理
}

動作:最初に反復条件を計算する. もし成立していたらブロック { } 内を処理し while へ戻る.

while はゼロ回以上の繰り返し. (ゼロ回って?条件不成立ならブロック内を処理しないので.)
なお,while 文は if 文と似ている. while は回数不明な繰り返しだが, if はゼロ回か1回だけの繰り返し,とも考えられる.

特殊な反復条件:

一方,if の場合... もちろん,こんな書き方は冗長. しかし,デバッグ作業では役立つかもしれない.
do-while 文:後判定条件反復

ソースコード記述方法:

do {
	繰り返したい処理
} while (反復条件);

動作:最初にブロック { } 内を処理し,その後で反復条件を計算する. もし成立していたら,do へ戻る.

do-while は 1回以上の繰り返しとなる.
反復の使い分け基準

反復回数に応じて,for/while/do-while を使い分けよう:

回数不明の場合にも for を使って短く書いてしまう技もありえます. しかし,コードの意図を理解しづらく,間違いを発見・修正しづらく, 困ったことになりますよ.
基本的には「keep it short and simple!」なのですが, 「短ければ短いほど良い」と云うものではありません. 「過ぎたるは及ばざるが如し」です. 「keep it simple, stupid!」と言われないようにしましょうね. KISSの原則
制御構造(今回の for 等だけでなく,前回の if 等)の使い分けについては, プログラムの開発効率にも動作効率にも大きく影響する場合があります. 適切な使用方法を心がけましょう.

本日の作業

タスク1:合計の計算

複数の非負整数データを合計するプログラム total.c を 3種類のループで作り分けてみよう.

Step 0. 準備する.
次回からは省略予定.

まずは,ディレクトリの準備:

$ mkdir c-0524
$ cd c-0524	または	cd !$

テンプレートをコピーして, ソースファイル total.c の編集を開始:

$ cp ~/tmpl.c total.c	# テンプレートを利用
$ vim total.c	# エディタは何でも OK(以下同様)

または,Vim 内でテンプレコピーして編集開始:

$ vim ~/tmpl.c
:f total.c
Step 1. for で作ってみる.
#include <stdio.h>

int main(void)
{
	int	total;	// 合計
	int	x;		// データ
	int	n, i;		// データの個数,カウンタ

	// データの個数の入力
	printf("データの個数 > ");
	scanf("%d", &n);		// 個数を入力

	// データの入力,合計の計算
	total = 0;				// 合計を初期化
	printf("%d 個の非負整数 > ", n);
	for (i = 0; i < n; i++) {
		scanf("%d", &x);	// データを入力
		total += x;		// 合計を加算,total = total + x;
	}

	// 結果の表示
	printf("合計 = %d\n", total);

	return (0);
}
$ cc total.c -o total

$ ./total
データの個数 > 5
5 個の非負整数 > 1 2 3 4 5
合計 = 15

個数の入力が面倒くさいですね. データが何個あるか入力前に数えておく必要があるなんて, 非実用的だろう. このように,for ループだと, 反復回数を最初に決めておかなければならないので仕方がない. 次に,while を使って書き換えてみよう.

いつでも for はダメ,とは言っていません. 目的によっては,for を使う方が実用的な場面もあります.
Step 2. while に変えてみる.
...
int main(void)
{
	int	total;
	int	x;
/*
	int	n, i;

	// データの個数の入力
	printf("データの個数 > ");
	scanf("%d", &n);
*/

	// データの入力,合計の計算
	total = 0;
	printf("複数個の非負整数(最後に -1)> ");
	x = 0;		// 初回の反復条件を成立させるための措置
	while (x >= 0) {
		scanf("%d", &x);
		total += x;
	}
	total -= x;	// 調整(最後の x = -1 は合計すべきデータではない)
			// ...本来,このような帳尻合わせはよろしくない

	// 結果の表示
	...
}
$ cc ...
$ ./total
複数個の非負整数 > 1 2 3 4 5 -1
合計 = 15

データ数が多い場合を考えれば, 使い勝手は for 版よりは良くなったよね?

しかし,調整の処理について, 正しく計算するために必要ではあるが,足した値を引いており, 無駄手間となっている. データ入力と合計計算の順序を交換すれば この調整処理は不要となる.改善版:

	x = 0;
	while (x >= 0) {
		total += x;
		scanf("%d", &x);
	}
//	total -= x;

ただし,ループの1回目に,入力するより前に,ゼロを足しており, 不自然かつ非効率だろう.

さらに,この処理手順の場合, 必ず1回以上はループ内の入力 scanf() が実行されるので, do-while の方が適切だ.

Step 3. do-while にも変えてみる.
...
int main(void)
{
	...
	// データの入力,合計の計算
	printf("複数個の非負整数(最後に -1)> ");
	x = 0;
	do {
		total += x;
		scanf("%d", &x);
	} while (x >= 0);

	// 結果の表示
	...
}

...

ソースコード的は while 版とほとんど変わりないが, 入力終了時にブロックの上方向へ戻る必要がなく, 処理手順的には,より効率的だ. しかしまだ,入力前の合計計算という不自然・非効率な処理が残っている.

この辺りの問題点については,次回「中判定ループ」で解決予定. 無条件ループ内で if-break を使うことになる. しかし今回は,break を使わずに作業する.
タスク2:反復による掛け算

反復の練習として,掛け算を足し算の反復で計算してみよう.

ソース mul.c:(total.c の Step 1 とほぼ同じ)

#include <stdio.h>

int main(void)
{
	int	x, y, z;	// x * y = z
	int	i;		// カウンタ

	printf("非負整数 x, y > ");	// 問題を入力
	scanf("%d %d", &x, &y);

	z = ...;			// 積を初期化
	// 反復回数は既知なので for文
	for (i = 0; i < y; i++) {	// y 回繰り返す
		z = ...;		// x を足す
	}

	printf("%d * %d = %d\n", x, y, z);	// 計算結果を表示

	return (0);
}
...」の部分のコードについては,各自で補うんですよ.

考え方のヒント:x*y = x + x + x + ... = (((0 + x) + x) + x) + ...

足し算の括弧が y 回, それぞれの括弧がループ1回分, 括弧内の計算結果が z ね.
$ cc ...
$ ./mul
非負整数 x, y > 12 3
12 * 3 = 36
タスク3:反復による割り算

割り算を引き算の反復で計算してみよう. ついでに剰余(余り)も計算するよ.

ソース div.c:(mul.cと類似)

#include <stdio.h>

int main(void)
{
	int	x, y, z;	// x / y = z
	int	m;		// 剰余 m = x % y

	printf("自然数 x, y > ");	// 問題を入力
	scanf("%d %d", &x, &y);

	z = ...;		// 商を初期化
	m = ...;		// 剰余を初期化(x をコピーしておく)
	// 反復回数は未知(割り算の結果)だし,ゼロかもしれないので while文
	while (m >= y) {	// 引き算が非負である限り反復
		m = ...;		// x から y を引く(実際には m から引く)
		z++;			// 引き算の回数をカウント
	}

	printf("%d / %d = %d ... %d", x, y, z, m);	// 計算結果を表示

	return (0);
}

考え方のヒント:引き算できた回数が計算結果となる.

なお,実際に x から y を引いてしまうと, x の元の値がわからなくなり,結果表示のときに困る. そこで,xm にコピーしておき,m から引くことにした.
$ cc ...
$ ./div
自然数 x, y > 23 4
23 / 4 = 5 ... 3
タスク4:総和の計算

総和 Sn = 1 + 2 + 3 + ... + n を求めるプログラム sum.c を作成せよ.

要求仕様:非負整数 n の数値を入力,総和 Sn の数値を表示する.

実行例:

$ ./sum
非負整数 n > 3
総和 Sn = 6

$ ./sum
非負整数 n > 10
総和 Sn = 55
入力データのエラーチェック(非負かどうか?)については省略してよい.

タスク5:最大値・最小値の抽出

試験の得点データ集合から 最大値と最小値を抽出するプログラム maxmin.c を作成せよ.

要求仕様:ゼロ個以上の整数値 0〜100 を入力, 負の場合に入力を終了. 最大値および最小値を表示する.

実行例:

$ ./maxmin
得点 > 75
得点 > 85
得点 > 40
得点 > -1

最高点 = 85
最低点 = 45
入力データのエラーチェック(100 以下かどうか?)については省略してよい.
余裕ある人は入力データの条件を無視してハードルを上げてみよう. どんな整数でもよいことにする. 最初のデータを最大値・最小値の初期値とすればよい. 終了方法には工夫とか妥協が必要かも.
タスク6:九九の計算表

小学生向け教材「九九の計算表」を表示するプログラム 99.c を作成せよ.

実行例:

$ ./99
1の段:  1  2  3 ...  9
2の段:  2  4  6 ... 18
3の段:  3  6  9 ... 27
...
9の段:  9 18 27 ... 81
ヒント:二重ループ(ループ内でループ)を使えば良い. カウンタ変数も2個にしなければならない.
数値の表示桁数を固定して,レイアウトを整えること.

本日の課題

レポートを提出せよ.

質問 Q1〜Q5 に回答し,電子メールで提出せよ.