情報基礎A 「Cプログラミング」(ステップ3・選択処理)
このステップの目標
- 分岐構造とプログラムの流れを的確に把握できる
- if文を使って、分岐のあるフローを記述できる
- C言語の条件式を正しく記述できる
1. 分岐のあるプログラムと選択処理
二次方程式を解くプログラム(ex3.c)
二次方程式 $a x^2 + b x + c = 0$ の係数 $a,b,c$ を与えると、判別式を評価し、実数解を表示するプログラムの例を以下に示す:
例題3(ex3.c)
この例題は数学関数(sqrt()
)を用いているので,
TurtleEditを使わない(Linuxコマンドで操作する)場合には,
コンパイルするときに(前回のヘロンの公式の計算プログラムと同様に)、
cc ex3.c -lm
のようにcc
コマンドの最後に -lm
(スペース・ハイフン・エル・エム)オプションを付けること
#include <stdio.h> #include <math.h> main( ) { float a, b, c, d, s1, s2 ; printf("係数 a b c を入力してください:") ; scanf("%f%f%f",&a,&b,&c) ; d = b*b - 4*a*c ; if (d<0) { printf("実数解はありません\n") ; } else { s1 = (-b + sqrt(d))/(2*a) ; s2 = (-b - sqrt(d))/(2*a) ; printf("答え: %f %f\n",s1,s2) ; } }
例題3のアルゴリズム
Input: 2次方程式の係数 $a,b,c$
Output: 2次方程式の2つの実数解
1: 画面に"係数a b cを入力してください:"と出力し、改行する
2: キーボードから3つの実数を入力し、それぞれ変数 $a, b, c$ にセットする
3: 実数の変数 $d$ に $b^2 - 4 a c$ の値をセットする
4: もし $d \lt 0$ ならば以下を実行する:
5: "実数解はありません"と出力し、改行する
6: それ以外($d \ge 0$)の場合は以下を実行する:
7: 変数 $s_1$ に $\frac{-b + \sqrt{d}}{2 a}$ の値をセットする
8: 変数 $s_2$ に $\frac{-b - \sqrt{d}}{2 a}$ の値をセットする
9: "答え: "に続いて、$s_1, s_2$ の値を出力し、改行する
10: 条件分岐はここまで
11: 終了する
プログラムを実行すると、二次方程式の係数の値を尋ねられるので、三つの数値を順にタイプしてEnterキーを押すと、実数解が(もしあれば)表示される。
if文を使った条件分岐(選択処理)
if文
2次方程式の解の求め方が、判別式の正負で異なるように、処理(計算)の内容を条件によって切替なければならない類の問題に、我々はしばしば遭遇する。 その際に、「切り分け」を行うのがif 文である。
if文には以下の二種類の基本形がある。
- if ( 条件式 ) 文1;
- if ( 条件式 ) 文1; else 文2;
この基本形の中で、文1; や 文2; の個所は、複文(ブロック)でも構わない。ブロックとは { 何々;何々;・・・;} という風に、複数の文を{ }でひとつにまとめた(段落のような)ものだった。 つまり、複文も「文」の一種と見なせば、
if ( 条件式 ) { 何々; 何々 ; ・・・ } else { 何々; 何々 ; ・・・ }
という書き方も、基本形2の一種というわけだ。 このことから、例題3は基本形2をひとつ含む例であることがわかる。
それぞれの基本形を流れ図で示すと以下のようになる;
条件式
基本形の条件式のところには、処理の流れを切り替えるための条件を、下表のような論理式のかたちで記述する。
そして、その「真」,「偽」に応じて、流れがコントロールされる。
表中で、a,bのところには、変数だけでなく、数式を記述することもできる。
条件式は、必ず丸括弧( )
で挟んおかなければならない。
書き方 | 内容 |
---|---|
( a == b ) |
aとbが等しければ・・・ 等号(=) 2つであることに注意 等号ひとつ(=)は「代入」と解釈され、全く意味が異なる。 |
( a < b ) |
bが大きければ・・・ |
( a > b ) |
aが大きければ・・・ |
( a <= b ) |
bがaより大きいか,等しければ・・・ " =< " はNG |
( a >= b ) |
aがbより大きいか,等しければ・・・ " => " はNG |
( a != b ) |
aとbが等しくなければ・・・ |
例題3のその他の見所
- bの2乗は
b*b
と書く(Cではb^2
やb**2
のような書き方は出来ない) - 平方根は sqrt(式) で表現(sqrtはSQuare RooTの略)。これを使うときは、プログラムの先頭部に
#include <math.h>
を忘れずに書いておく。 (-b + sqrt(d))/(2*a)
と(-b + sqrt(d))/2*a
は結果が異なる。数式中の括弧の使い方に注意。
if文の組み合わせによる複雑な分岐処理
例題3のプログラムは、係数aに0を入れると(一次方程式の場合は)、解の公式の分母が0になってしまうので、正しく計算を行うことができない。 そんな場合にも対応するには、aが0の場合と、それ以外の場合で、処理方法を変えなければならない。具体的には、
もしaが0ならば、一次方程式の解を求める そうでなければ(aが0以外ならば)、以下の処理を行う: もし判別式が負値ならば、実数解なし そうでなければ、解の公式で二つの実数解を求める
といったアルゴリズムになるはずだ。これをCのプログラムに「翻訳」すると、
さらに細かいことを言えば、bが0の場合も場合分けする必要がある。
if (a==0) { s1 = -c/b ; printf("答え: %f\n",s1) ; } else { if (d<0) { printf("実数解はありません\n") ; } else { s1 = (-b + sqrt(d))/(2*a) ; s2 = (-b - sqrt(d))/(2*a) ; printf("答え: %f %f\n",s1,s2) ; } }
となる。if文の基本形2のelseの「内部」に、さらに基本形2のif文が挿入されている点に注目。
字下げ(indent)
字下げのスタイルにも色々な流儀はあるが、基本的な作法は是非身につけたい。
この例のように、基本形の「何々」中にさらにif文が「入れ子」になった ネスティング(nesting)構造になる場合が多い。
そこで、if文を書くときには、基本形の各パーツ毎に字下げを行って、ifとelse、括弧の対応、そして処理の流れをビジュアル的に分かりやすく記述する習慣をつけよう。
さもないと、コードはぐちゃぐちゃになって、それを書いた本人でさえ、判読不能に陥るだろう。
C言語では、予約語(int, float, if, else
等)、変数名、関数名(main, printf
等)以外の場所なら、どこにスペースや改行を置いても、
動作には全く影響しない。
見かけ上もわかりやすく記述できるかどうかは、プログラム作成者の大切なセンスのひとつだ。
練習:重根および虚数解にも対応できるプログラム
例題2を元に、重根($d=0$)の場合には、根をひとつだけ出力するようにプログラムを改良してみなさい。
余力があれば、さらに $d \lt 0$ (虚数解)の場合であっても、"実部 + 虚部 i" のように計算結果が表示されるよう、プログラムを拡張してみなさい。
ヒント
実部と虚部をそれぞれ別個に計算しておいて、printf("%f + %f i \n", real1, imag1) ;
のように表示すれば良さそうだ。
その際、追加の変数が必要であれば、適宜宣言すること。
発展:三次方程式
さらなる発展として、三次方程式の解の公式(カルダーノの公式等)を使って、
三次方程式を解く(とりあえずは、実数解のみ)プログラムに挑戦してみるのも面白いだろう。
式の途中で、三乗根の計算が必要になるが、Cでは、実数変数x
の三乗根はpow(x,1.0/3.0)
と表現すればよい
(pow(x,y)
は$x^y$を表す関数)。
2.複雑な分岐条件の記述
上の節では、if文の構成方法にフォーカスを当てたが、if (条件式) {何々;}
の (条件式) のパートの書き方にも、色々なバリエーションが可能だ。
書き方の例 | コメント |
if (a+b > c+d) ・・・ |
数式の計算結果同士の比較の例 |
if ( k%2 ) ・・・ |
ここでkはint型と仮定。k%2は、kを2で割った余り。 kが偶数の場合はk%2は0を、奇数の場合はk%2は1を与える。 C言語の条件式では、整数式の結果が0の場合は「偽」を、 それ以外(例えば1)の場合は「真」と解釈される。 つまり、この例は、「もしkが奇数ならば・・・」のC言語流の書き方 |
if ( a*a + b*b == c*c ) ・・・ |
a,b,cがint型の場合には、見てのとおりの条件式。 a,b,cがfloat型の場合(授業で説明はしていないがdouble型の場合も)、この条件式は 意図した通りに働かない可能性が高い。というのは、コンピュータは実数の 近似値を計算しているだけなので、数学的には等号が成立する場合でも、 誤差によって、計算機の内部では両辺が「完全に」等しい保証がないからだ。 例えば、 float x=2; if (sqrt(x)*sqrt(x)==x)・・・は「偽」になってしまう。 |
複雑な条件式
論理積(かつ)、論理和(または)、否定(でない)
込み入った条件を記述しようとすると、以下のように、論理積(「かつ」)、論理和(「または」)、論理否定(「・・でない」)の組み合わせ が必要になることも多い。ここでは、以下の3つの基本的なパターンをきっちりと押さえておこう:
条件 | 書き方の例 | コメント |
A かつ B |
( a > b && b > c ) |
& が二つで「かつ(and)」を表す。左の例を (a > b > c )と書いてはいけない! |
A または B |
( a > b || a > c ) |
| が二つで「または(or)」を表す。 |
A ではない |
( !(a > b) ) |
! は、その右側の条件式の否定(negation, not)。否定すべき部分全体を括弧で囲むこと。 |
練習:数値の並べ替え
キーボードから3つの実数を入力すると、それを大きい順(降順)に並べ替えた上で表示するプログラムを作成してみなさい。 こちらの課題で考えたアルゴリズムを参考にすればよい。
#include <stdio.h> main() { float a,b,c,t ; printf("a b c :") ; scanf("%f%f%f",&a,&b,&c) ; if ... ????? ... printf("a= %f b=%f c=%f\n",a,b,c) ; } |
練習:三角形の分類
三角形の三辺の長さを入力すると、それが鋭角三角形(acute triangle)か、直角三角形(right triangle)か、鈍角三角形(obtuse triangle)かを判別し、結果を出力するプログラムを作成しなさい。
ヒント
三辺の長さを $a,b,c$ としたとき、$a$ が最も長い辺の場合は、$a^2 \lt b^2 + c^2$なら鋭角、$a^2 \gt b^2 + c^2$なら鈍角、 $a^2 = b^2 + c^2$ なら直角三角形ということになる。 だが、入力された3つの数値のうち、いつも $a$ が最長になっているとは限らない。 そこで、まず $a$ が最長になるように並べ替えてから、$a^2$ と $b^2+c^2$ の大小関係を評価すればよい。 その際、$b \gt c$である必要はない。
#include <stdio.h> main() { float a,b,c,t ; printf("a b c :") ; scanf("%f%f%f",&a,&b,&c) ; /* 辺の長さの並べ替え(aが最長になるように) */ ????? ... /* 確認ステップ */ printf("並べ替え後の辺の長さ: a= %f b=%f c=%f\n",a,b,c) ; /* 三角形の分類と結果の出力 */ ????? ... } |
練習:三角形の面積計算(チェック機能付き)
ステップ2の練習課題で、 三角形の3辺の長さa,b,cから、その面積Sを計算するプログラムを作成した。 そのときは、あまり気にしなかったけれども、a,b,cの組をあまりいいかげんに設定すると、そもそも 三角形が構成できないので、面積を計算すること自体に意味が無くなってしまうだろう(そして、ヘロンの公式の根号の中が負になってしまう)。 そこで、入力したa,b,cが三角形を構成できない場合には「三角形が構成できません」と表示して終了するように、 面積計算のプログラムを拡張してみなさい。
ヒント
三角形が構成できる条件は、全ての点の組み合わせについて三角不等式(triangle inequality) $a+b>c,\; b+c > a,\; c+a>b$ が(いずれも)成り立つこと。 一般に、(その意図のある無しに関わらず)間違ったデータを入力された場合を想定して、 あらかじめエラー回避を行う処理を仕込んでおく配慮が大切だ。
三角形が構成できないような辺の組合わせでヘロンの公式を使うと、平方根関数の(sqrt( )
)中が負になってしまう。
こうした際には、当然、結果を数値として表現できないから、コンピューターは「それは数ではない」という意味で nan (not a numberの頭文字) と表示する場合がある。
発展練習:うるう年の判定
参考:暦の計算の基本事項
西暦年を入力すると、グレゴリオ暦(Gregorian calendar)に従って、それがうるう年(leap year)かどうかを判定し、「平年です」または「うるう年です」のように出力するプログラムを作成しなさい。
ヒント
地球の公転周期は約365.2422日であることが分かっている。 現在採用されているグレゴリオ歴では、 基準となる日数を365日として、西暦年が
- 4で割り切れたら +1 日 (4年に1度の+1日調整、すなわち 1年あたり +1/4 日の調整)
- 100で割り切れたら -1日(100年に1度の-1日調整、すなわち 1年あたり -1/100 日の調整)
- 400で割り切れたら +1日(400年に1度の+1日調整、すなわち 1年あたり +1/400 日の調整)
のルールで調整し、平均的な1年の長さが、実際と非常に近い、$365 + \frac{1}{4} - \frac{1}{100} + \frac{1}{400} = 365.2425$ 日となるように工夫されている。 そして、うるう年とは、『調整日数が 0 日以外』であるような年のことである。 ただし、『調整日数が0日以外』は、『4で割り切れる または 100で割り切れる または 400で割り切れる』を意味しないことに注意。 何故なら、調整日数が +1-1=0 となる組み合わせもあるからである。
詳しくは、暦の計算の基本事項を参照のこと。
剰余
yが4で割り切れるかどうかを判断するには、
if ( year%4 == 0) { ・・・ }
といった具合に、整数の剰余を計算する演算子%
を使えばよい。たとえば
8%4
は0
を与え、 9%4
は1
、 10%4
は2
を与える。
(なお、負の数の剰余の定義は言語処理系によって流儀が異なる場合があるので、注意が必要である。)
以下に、出発点となるひな形を示しておく:
#include <stdio.h> main() { int year ; printf("year ?") ; scanf("%d",&year) ; if.... ????? ... } |
発展:曜日の計算
暦と日付の計算の説明を読んで、西暦年月日(y, m, d)を入力すると、 その日の曜日を出力するプログラムを作成しなさい。
亀場で練習:三角形の描画(チェック機能付き)
以前に作成した三角形の描画プログラムを改良し、 3辺の長さa,b,cを与えると、三角形が構成可能な場合は、 直角三角形ならば白、鋭角三角形ならば青、鈍角三角形ならば赤色で、亀場に描くプログラムを作成しなさい。 また、もし三角形が構成できない場合は、"NO SUCH TRIANGLE" と亀場に表示するようにしなさい。
ヒント:
線分の色を変えるには、PD()
でペンを下ろす前に COL()
関数を呼び出す。
色の使用について、詳しくはこちらのページを参照のこと。
また、亀場に文字列を描くには SAY("ABCEDFG...")
関数を使う。