条件分岐と繰り返し
今回は、条件分岐と繰り返しについて、少し進んだトピックを説明し、練習を行う。
論理演算子(教科書5.6節)
不等式で 10 <= x < 100 で表される範囲は、10 <= x かつ x < 100 と考えて、if文の組み合わせで書けることは説明した。では、x < 10, 100 <= x 、つまり、x < 10 または 100 <= x で表される範囲はどうだろうか?
x < 10 と 100 <= x はどちらが成立しても同じ処理を実行するのだから、if文を使って次のように書ける:
if (x < 10) { [条件が成立したときに実行する処理] } if (100 <= x) { [条件が成立したときに実行する処理] }
[条件が成立したときに実行する処理]は同じ内容であることに注意。
比較演算子とif文だけを使って「または」を実現するにはこのように書くしかないが、一見無関係な条件が並べて書かれているように見えてしまったり、条件が成立したときに実行する処理を同じように書かなければならないため、プログラムにミスが生じやすい。
なお、このようなミスを防ぐには「ややこしいな」と感じた場所には極力コメントで説明を書いておくべきである。
そこで、複数の条件式を組み合わせる論理演算子を使う。基本となる論理演算子は次の3つである。
![条件式1]
[条件式1]でない(NOT、否定)
[条件式1]が真の場合偽、偽の場合真
[条件式1] && [条件式2]
[条件式1]かつ(AND、論理積)[条件式2]
[条件式1]と[条件式2]が共に真の場合真、それ以外の場合偽
[条件式1] || [条件式2]
[条件式1]または(OR、論理和)[条件式2]
[条件式1]と[条件式2]のどちらかが真の場合真、共に偽の場合偽
&&と||は2個連続で書くことに注意。1個だけの&や|は別の意味の演算子になってしまう(コンパイルエラーにはならない)。
それぞれを図示すると次のようになる:
AND演算子とOR演算子を使うと、10 <= x < 100 と x < 10, 100 <= x はそれぞれ次のように書ける。
if (10 <= x && x < 100) { [条件が成立したときに実行する処理] }
if (x < 10 || 100 <= x) { [条件が成立したときに実行する処理] }
それぞれ、数直線で描くと次のようになる。
演算子の優先度は比較演算子の方が論理演算子よりも高いので、&& や || の両辺の比較式が先に評価される。分かりにくければ、
(10 <= x) && (x < 100) のように、()を書くとよいだろう。
不等式は、例えば 10 <= x を x >= 10 と書いても意味は同じである。分かりやすい書き方を選べばよい。本資料の著者は、範囲を記述する場合は不等式と同じになるように書くようにしている。但し、書き方によって若干実行速度が速くなったり遅くなったりすることがある。これは極限状態のプログラムを書かなければならないときに気をつければよいことであって、通常は読みやすさを優先して書くべきである。
NOT演算子は対象となる条件を否定する。例えば、
!(x == y)
は、x == y(xとyが等しい)を否定するので、「xとyが等しくない」という意味になる。すなわち、次の条件と同じ意味になる。
x != y
また、上記の例に!をつけてみる。()に注意。!は優先順位が低いので、条件式全体を否定するために()で括る必要がある。
!(10 <= x && x < 100)
これは「xが10以上かつxが100未満、ではない」、つまり「xが10未満またはxが100以上」という意味になるから次の条件と同じになる(=の有無に注意)。
x < 10 || 100 <= x
以上のようにNOT演算子を使う代わりに、条件と論理演算子を変更することで同じ意味になる条件式を書くことができる。変形の仕方は数学の授業に譲りここでは省略する。ただしNOT演算子を使った式の方が人間が理解しやすい場合もあるので、上手く使おう。
練習:次の条件を論理演算子を使って表してみよう。ただし、xとyは整数型の変数である。
1. xが12以上、かつ、xが34以下
2. xが12以下、または、xが34以上
3. xが10より大きい、かつ、yが10より小さい
4. xとyが共に負
5. xが3で割り切れ、5でも割り切れる
6. xとyの少なくともどちらかは0
7. xは正の偶数
8. xとyは共に正の数、または、xとyは共に負の数
switch-case文(教科書5.5節)
ある変数や式の値に対して、複数の整数値に応じて処理を振り分けたい場合、switch-case文という文法が用意されている。基本的な書き方は次のようになる:
switch (整数式) { case 整数定数1: 実行式1_1; 実行式1_2; .... break; case 整数定数2: 実行式2_1; 実行式2_2; .... break; .... .... default: 実行式n_1; 実行式n_2; .... }
switchの後の()内の値に一致するcase文の箇所に処理が飛び、その次に書かれている実行式が順に実行される。一致するcase文が無い場合はdefault:に飛ぶ。default:は特にすることがなければ省略可能であるが、念のため書いておく方がよいだろう。break;に達したらswitch文の{に対応する}の次に移動する(途中の処理を飛ばす)。case文の整数定数の後ろは:(コロン)であることに注意!!
break;を省略すると、処理を中断せずに次のcase文の次に書かれている実行文を続けて実行することになる。プログラムによってはそのような書き方をする場合もあるが、break;を意図的に省略したことをコメントで必ず書いておくべきである。
switchとcaseを同じ位置に揃えるインデントのしかたや、caseやdefaultの:の後に最初の実行式を書くなど、書き方は様々なバリエーションがある。自分で見やすいと思う書き方を選べばよい。
条件分岐の回の「複数の値との比較」の項で紹介した宝箱の例をswitch-case文で書くと次のようになる:
switch (box) { case 1: printf("宝箱は罠だった\n"); printf("敵が現れた\n"); break; case 2: printf("あなたは宝を手に入れた\n"); break; case 3: printf("宝箱は空っぽだった\n"); break; default: // 何もしない break; // 2023.6.14追記: break;を入れました。何もないとコンパイルエラーになります }
このような条件分岐はif-elseを使っても同じように書けるが、switch-caseを使った方がプログラムの意図をはっきりさせることができる。初心者には少々ややこしいが積極的に使ってみて覚えておこう。
switch-case文を使えないケースとしては、値の範囲で条件分岐させたい場合や、変数や計算式が実数型の場合などがある。
2重ループ(教科書6.4節)
繰り返しの中に繰り返しを入れることもできる。このような構造を2重ループ、あるいは入れ子構造、ネストnestなどと呼ぶ。2重ループ自体はC言語の文法が特別に用意されているような特殊なものではなく、単に繰り返しを組み合わせただけであるが、プログラミング初心者がつまづきやすいポイントのようである。考え方をしっかり理解しておこう。
まず、具体例を見てみよう:
/***** doubleLoop.c 繰り返しの入れ子の例 M.Minakuchi *****/ #include <stdio.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"); } return 0; }
処理がどのように実行されるかを予想しながら入力し、コンパイル・実行して動作を確認しよう。
どのように処理が進むかは、順次実行と繰り返しのルールに従って、1行ずつ順番に追いかけていけばよい。また、各変数の値がどのように変わっていくかをメモしながら丁寧に追いかけてみること。内側の繰り返しが一通り終わってから、外側の繰り返しが1回進む、というのがポイント。
内側と外側で別々のカウンタ変数を使っていることに注意。同じカウンタ変数を使ってしまうと、思った通りに繰り返してくれなくなる。
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文(教科書6.5節)
繰り返しはwhile文のみで書くことが可能であるが、複雑になりすぎる場合もある。そのような場合、break文とcontinue文を使うことで簡単に書けることがある。これらは、繰り返しの途中でも何かの条件が成立したときに繰り返しの流れを変化させることができる。
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++; }
実行させるとどのような結果になるか?正しく動作させるにはどのように修正すればよいか?
確認課題
以下、出来たら教員に確認を受けること。原則的に易しい順となっているが、順序どおりに取り組む必要はない。また、多目に用意しているので授業時間内にすべてができることを求めるものではない。Do your best!
確認課題1. 3の倍数でfizz
まず正の整数を入力させる。次に、1からその数まで順に1行ずつ整数値を表示するが、3の倍数の時は値の代わりにfizzと表示するプログラムfizz.cを作成せよ。
【実行例。下線部は入力値の例。】 % gcc -o fizz fizz.c % ./fizz input number: 7 1 2 fizz 4 5 fizz 7 %
ヒント:手順を図示すると次のようになる。繰り返しの中で、表示を切り替えるために条件分岐を使う。
確認課題2. 色つき同心円
大きさ600×600のウィンドウに、中心座標が(300, 300)、半径が50, 100, 150, 200, 250の同心円を5つ、内側から数えて奇数個目の円は青、偶数個目の円は赤で描くように繰り返しを使って描くプログラムconcentricColor.cを作成せよ。
ヒント:円を描く直前で青か赤の条件を判定して色を設定する。どちらの色で描くかを決めるための変数を追加する方法もある。
確認課題3. 合計
整数値を入力させ、合計を計算するプログラムsum2.cを作成せよ。ただし、入力値が0の時にそれまでの入力値の合計を表示してプログラムを終了する。
【実行例。下線部は入力値の例。】 % gcc -o sum2 sum2.c % ./sum2 45 58 92 77 0 合計は272 %
ヒント:手順を図示すると次のようになる。無限ループで繰り返し、入力値が0の時に繰り返しを抜ける(breakを使う)。sumはカウンタ変数でないことに注意。
確認課題4. 平均
整数値を入力させ、平均値を計算するプログラムaverage2.cを作成せよ。ただし、入力値が0の時にそれまでの入力値の平均値を表示してプログラムを終了する。平均値は小数点以下切り捨てでよい(int型で計算すると自動的にそうなる)。
【実行例。下線部は入力値の例。】 % gcc -o average2 average2.c % ./average2 45 58 92 77 0 平均は68 %
ヒント:sum2.cをコピーして修正すればよい。平均=データの合計÷データの個数
確認課題5. 範囲判定
まず、最小値と最大値の、2つの整数を小さい順に入力させる。そして、別の値を入力させ、その入力値が最大値と最小値の範囲内である限り入力を繰り返させるプログラムrange.cを作成せよ。
【実行例。下線部は入力値の例。】 % gcc -o range range.c % ./range 最小値: 4 最大値: 10 判定値: 5 判定値: 8 判定値: 9 判定値: 11 %
繰り返しの回のdo-whileを参照。条件式はどのように書けばよいか?
確認課題6. 範囲判定・改
まず2つの整数を入力させる。そして、この2つの値の範囲内である限り入力を繰り返させるプログラムrangeKai.cを作成せよ。
【実行例。下線部は入力値の例。】 % gcc -o rangeKai rangeKai.c % ./rangeKai 1つめの範囲値: 10 2つめの範囲値: 4 判定値: 5 判定値: 8 判定値: 9 判定値: 11 %
確認課題5.「範囲判定」において、最初の2つの入力値を比較して最小値と最大値を決めておく。
確認課題7. fizzbuzz [advanced]
まず正の整数を入力させる。次に、1からその数まで順に1行ずつ整数値を表示するが、3の倍数の時は値の代わりにfizz、5の倍数はbuzzと表示するプログラムfizzbuzz.cを作成せよ。3の倍数でもあり5の倍数でもある場合はfizzbuzzと表示する。
【実行例。下線部は入力値の例。】 % gcc -o fizzbuzz fizzbuzz.c % ./fizzbuzz input number: 20 1 2 fizz 4 buzz fizz 7 8 fizz buzz 11 fizz 13 14 fizzbuzz 16 17 fizz 19 buzz %
fizz.cをベースに、条件を更に増やしてみよう。3と5の両方の倍数は幾つかの判定方法が考えられる。
これは初級のプログラミング能力を見る問題として有名である。
確認課題8. KSU
整数値を入力させ、入力された回数だけ縦横方向にKSUを繰り返して表示するプログラムksu.cを作成せよ。
【実行例。下線部は入力値の例。】 %gcc -o ksu ksu.c % ./ksu 回数を入力してください: 4 KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU % ./ksu 回数を入力してください: 6 KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU KSU %
ヒント:2重ループを使う。横方向に繰り返しで改行せずに表示し、1行分表示し終わったら改行だけ表示して次の行に進む。”KSU "と最後にスペースを入れて表示するとよい。
確認課題9. ○○○○○ [advanced]
整数値を入力させ、大きさ480×480のウィンドウに、入力された数だけ縦横方向に円を敷き詰めて描くプログラムgridCircles.cを作成せよ。入力値によっては円の間やウィンドウの端に余白が生じても構わない(int型で計算すると誤差でそうなる)。
なお、計算方法および繰り返しの条件の書き方によっては計算誤差が蓄積して、入力値で指定した個数よりも1つ多く描いてしまうプログラムになることがある。今回はこのような場合も気にしなくてよい。
8の場合
7の場合
ヒント:横方向と縦方向の繰り返しの2重ループを使う。最初に半径を計算しておき、繰り返し回数に応じて中心座標を計算する。あるいは、中心座標を変化させて繰り返してもよい(同心円の例題を参照)。
確認課題10. ○●○●○ [advanced]
整数値を入力させ、大きさ480×480のウィンドウに、入力された数だけ縦横方向に円を敷き詰めて、横方向に白と黒で交互に描くプログラムgridCirclesWB.cを作成せよ。入力値によっては円の間やウィンドウの端に余白が生じても構わない(int型で計算すると誤差でそうなる)。
計算方法および繰り返しの条件の書き方によっては計算誤差が蓄積して、入力値で指定した個数よりも1つ多く描いてしまうプログラムになることがある。今回はこのような場合も気にしなくてよい。
ヒント:gridCicles.cにおいて、横方向の繰り返し回数が偶数の時は黒丸を描く。
本日の提出課題
提出課題1. 領域判定
まず円の中心座標を入力させる。大きさ600x400のウィンドウを開き、左下の座標が(150, 100)、幅が300、高さが200の長方形を描く(塗りつぶしでなくてよい)。入力された中心座標に半径50の大きさの円を描くとして、円の中心が長方形内の場合は赤色で、長方形の線上か外の場合は青色で塗りつぶして描く(輪郭は描いても描かなくてもよい)プログラムcheckArea.cを作成せよ。
提出期限:次の土曜日の24:00まで
【実行例(下線部は入力例、それぞれの表示は順に下図の通り)】 % hgcc -o checkArea checkArea.c % ./checkArea x: 300 y: 200 % ./checkArea x: 160 y: 180 % ./checkArea x: 150 y: 160 % ./checkArea x: 140 y: 140
% ./checkArea x: 200 y: 50
腕に覚えのある人は、まず上の説明だけで考えて作ってみて欲しい。どこから手をつけてよいか分からない人は、次のように考えてみよう:
次の手順で実行するプログラムcheckArea.cを作成せよ。
- 円の中心座標を覚えておくための整数型の変数x, yを宣言する。
- xとyの値をそれぞれ入力させる。
- 大きさ600x400のウィンドウを開く。
- 左下の座標が(150, 100)、幅が300、高さが200の長方形を描く(塗りつぶしでなくてよい)。
- xの値が150 < x < 450の範囲であり、かつ、yの値が 100 < y < 300の範囲の場合は塗りつぶし色を赤に設定する。そうでない場合は塗りつぶし色を青に設定する(最初の条件を論理演算子を使って書く。そうでない場合はelseを使う)。
- 中心が(x, y)、半径が50の塗りつぶし円を描く(輪郭は描いても描かなくてもよい)
- HgGetChar();で入力を待つ(プログラムが終了してしまわないようにするため)
- ウィンドウを閉じてプログラムを終了する
なお、「xの値が150 < x < 450の範囲であり、かつ、yの値が 100 < y < 300の範囲の場合は塗りつぶし色を赤に、そうでない場合は青に設定する。」の部分は、次のような手順にしてもよい。あるいは、同じ結果が得られるのであればこれら以外の処理方法(例えばelseを使う代わりに、それぞれの条件を個別に判定する)でも構わない。
- 塗りつぶし色を青に設定する
- xの値が150 < x < 450の範囲であり、さらに、yの値が 100 < y < 300の範囲の場合は塗りつぶし色を赤に設定する(設定し直す)
- 円を描く
提出課題2. ピラミッド
整数値を入力させ、大きさ600 x 600のウィンドウに、1段目は1個、2段目は2個、3段目は3個……と繰り返して入力された値の数だけ繰り返して円を描くプログラムpyramid.cを作成せよ。入力値のエラーチェックはなくてもよい。
入力値によっては円の間やウィンドウの端に余白が生じることがある(int型で計算すると誤差でそうなる)。また、計算方法および繰り返しの条件の書き方によっては計算誤差が蓄積して、入力値で指定した個数よりも1つ多く描いてしまうプログラムになることがある。今回は気にしなくてよい。
提出期限:次回授業開始時まで
(入力値が12の例)
ヒント:この課題の前に確認課題9をやるとよい。2重ループを使う。次のフローチャートを参考にせよ(これ以外の方法で描いても構わない)。HandyGraphicでは左下が原点であることに注意(円の中心座標の計算がちょっとだけややこしいです)。