第4章 プログラムの流れの繰り返し(1)
はじめに
人生は、日々の繰り返しである。何ごとにおいても『まったくの初めて』ということはあまりないであろう。同じことの《繰り返し》もあるし、よく似たことの《繰り返し》もある。
もっとも、人生のすべての瞬間が、新たなる発見の連続というのが理想なのかもしれないが…。
本章では、プログラムの流れの《繰り返し》を学習する。
4-1 do文
プログラムの流れを繰り返すための手段として、C言語は3種類の文を提供する。最初に学習するのは、do文である。
do文
前章で作成した、整数値の奇数/偶数を判定するプログラム(List 3-4:p.47)は、整数値の読込みと判定結果の表示が1回に限られている。次のように書き換えよう。
整数値を読み込んで、それが奇数か偶数かを判定表示する。その後、再び行うかどうかの確認を促して、入力と表示を好きなだけ繰り返して行えるようにする。
List 4-1に示すのが、そのプログラムである。プログラムを起動し直すことなく、入力と表示が好きなだけ繰り返せるようになっている。
// 読み込んだ整数値は奇数であるか偶数であるか(好きなだけ繰り返せる)
#include <stdio.h>
int main(void)
{
int retry; // 処理を続けるか
do {
int no;
printf("整数を入力せよ:");
scanf("%d", &no);
if (no % 2)
puts("その数は奇数です。");
else
puts("その数は偶数です。");
printf("もう一度? [Yes…0/No…9]:");
scanf("%d", &retry);
} while (retry == 0);
return 0;
}
水色の部分は、doで始まり、文(複合文)をはさんで、while (式);で終わっている。これが、Fig.4-1の構文をもつdo文(do statement)である。
【Fig.4-1】do文の構文図
先頭のdo
は『実行せよ』という意味で、while
は『~のあいだ』という意味である。
do文は、『( )の中に置かれた式(制御式)の評価で得られた値が真(非0)のあいだ、文を繰り返し実行せよ』と働く文である。
そのため、プログラムの流れは、Fig.4-2のように制御される。
【Fig.4-2】do文のプログラムの流れ
なお、繰り返しをループ(loop)ということから、繰返しの対象となる文は、ループ本体(loop body)と呼ばれる。
Note
この後で学習するwhile文とfor文が繰り返しの対象とする文も《ループ本体》である。
補足情報
ループ処理はプログラミングの基本構造の一つで、反復タスクを効率的に処理するために不可欠です。実際の開発では、ループ処理はデータ処理やユーザーインターフェースの制御など多くの場面で使用されます。
本プログラムのループ本体は、{から}までの複合文(ブロック)である。
その複合文では、奇数/偶数の判定表示を行って、「もう一度? [Yes…0/No…9]:」と確認を促した上で、変数retryに整数を読み込む。
ループ本体が終了すると、プログラムの流れは、制御式にさしかかり、その評価が行われる。
【[Fig.4-3】本プログラムのdo文
-
変数retryが0であれば… 制御式retry == 0の評価で得られる値は1である。その1は真であるから、ループ本体である複合文が再び実行される(Fig.4-3)。
-
変数retryが非0であれば… 制御式retry == 0を評価した値は0である。0は偽であるから、do文の実行は終了する。
Note
真と判定された場合は、プログラムの流れが複合文の先頭へと戻り、再び複合文が実行される。
制御式retry == 0が真すなわち非0であるかどうかが、繰り返しを続けるための《継続条件》であることが分かった。
コーディングのコツ
do文を使うと、ユーザーからの入力を検証し、条件を満たすまで繰り返し入力を求めることができます。これはユーザー入力の妥当性検証に非常に便利なパターンです。
複合文内での宣言
キーボードから読み込んだ値を格納する変数noは、ループ本体である複合文(ブロック)の中で宣言されている。複合文の中でのみ使う変数は、その中で宣言するのが原則である。
重要
複合文の中でのみ使う変数は、その複合文内で宣言する。
Note
複合文の構文は『{ 0個以上の文または宣言の並び }』であった(Fig.3-10:p.60)。
読み込む値を制限する
do文をうまく使うと、キーボードから読み込む値に制限を加えられる。List 4-2に示すのが、そのプログラム例である。
// 読み込んだ整数値に応じてジャンケンの手を表示(0、1、2のみを受け付ける)
#include <stdio.h>
int main(void)
{
int hand; // 手
do {
printf("手を選んでください [0…グー/1…チョキ/2…パー]:");
scanf("%d", &hand);
} while (hand < 0 || hand > 2);
printf("あなたは");
switch (hand) {
case 0: printf("グー"); break;
case 1: printf("チョキ"); break;
case 2: printf("パー"); break;
}
printf("を選びました。\n");
return 0;
}
まずは実行して動作を確認しよう。読み込んだ値に応じて、次のように動作する。
- 不当な値(3や-2など)→ 再び入力するよう促される。
- 妥当な値(0と1と2のいずれか)→「グー」、「チョキ」、「パー」が表示される。
それでは、do文の繰り返しの継続条件である制御式(水色の部分)に着目する。
論理OR演算子||
の判定は、『または』『いずれか一方でも』というニュアンスであった。
そのため、変数handの値が不当な値(0より小さいかまたは2より大きい値、すなわち、0、1、2以外の値)であれば、この判定は真となって成立する(制御式の評価でint型の1が得られるからである)。その結果、ループ本体が再び実行される。
Note
「手を選んでください [0…グー/1…チョキ/2…パー]:」と表示されて、入力が促される。
なお、handの値が0と1と2のいずれかの妥当な値であれば、繰り返しは終了する。そのため、do文が終了したときのhandの値は、必ず0と1と2のいずれかになる。
Note
do文の後ろに置かれたswitch文では、変数handの値に応じてジャンケンの手を表示する。
具体例
入力値の検証パターンは幅広く応用できる。例えば、「1から10までの整数を入力させる」場合は while (num < 1 || num > 10)
という条件式で制限できる。
論理否定演算子とド・モルガンの法則
『handが不当な値であれば…』という継続条件を、『handが妥当な値でなければ…』に書き換えてみよう。次のようになる("chap04/list0402a.c")。
初登場の!
は、論理否定演算子(logical negation operator)と呼ばれ、Table 4-1に示すように、オペランドが0と等しいかどうかを判定する演算子である。
Note
すなわち、上記の式は、(hand >= 0 && hand <= 2) == 0と同じである。
【Table 4-1】論理否定演算子
演算子 | 記法 | 説明 |
---|---|---|
論理否定演算子 | ! a | aが0であれば1、そうでなければ0(その値はint型)。 |
ド・モルガンの法則
Fig.4-4を見ながら、理解を深めよう。
【Fig.4-4】do文の継続条件と終了条件
オリジナルの制御式①は、繰り返しを続けるための継続条件である。一方、論理否定演算子!を使って書き換えた制御式②は、繰り返しを終了するための終了条件の否定である。
両者はコインの裏表であって、同じ条件を意味している。文脈に応じて、分かりやすい、読みやすい、と感じられるほうの式を使うとよいだろう。
なお、『各条件の否定をとって、論理積・論理和を入れ替えた式』の否定が、もとの条件と同じであることは、ド・モルガンの法則(De Morgan's theorem)として知られている。
この法則を、C言語の演算子を使って表すと、次のようになる。
- x && y と !(!x || !y) は等しい。
- x || y と !(!x && !y) は等しい。
注意
- x && y ⇒ !!(x && y) ⇒ !(!x || !y)
- x || y ⇒ !!(x || y) ⇒ !(!x && !y)
補足情報
ド・モルガンの法則は論理学における重要な原則で、プログラミングだけでなくデジタル回路設計などでも活用されている。複雑な条件式を単純化する際に役立つ。
複数の整数値の合計と平均を求める
do文を使って、次のプログラムを作ろう。
整数値を次々と読み込んでいき、その合計と平均を表示する。
List 4-3に示すのが、そのプログラムである。
// 整数値を次々と読み込んで合計と平均を表示(その1)
#include <stdio.h>
int main(void)
{
int sum = 0; // 合計
int cnt = 0; // 整数値の個数
int retry; // 処理を続けるか
do {
int t;
printf("整数値を入力せよ:");
scanf("%d", &t);
sum = sum + t; // sumにtを加えた値をsumに代入(sumにtを加える)
cnt = cnt + 1; // cntに1を加えた値をcntに代入(cntに1を加える)
printf("まだ?<Yes…0/No…9>:");
scanf("%d", &retry);
} while (retry == 0);
printf("合計は%dで平均は%.2fです。\n", sum, (double)sum / cnt);
return 0;
}
Note
本プログラムのdo文は、List 4-1(p.74)と同じ構造である。キーボードから変数retryに読み込んだ値が0である限り、何度も繰り返しを続ける。
右ページのFig.4-5を見ながら、合計を求めていく流れを理解していこう。
【Fig.4-5】合計を求めていくプログラムの流れ
- 準備(合計と個数の初期化)
合計を求めるための準備である。読み込んだ整数値の個数を格納する変数cntと、合計を格納する変数sumの両方を0にする。
- 合計と個数の更新
ループ本体内で、変数tに整数値を読み込んだ後に行う代入である。
注釈に書かれているように、sumにtが加えられて、cntには1が加えられる。
実行例の場合、1回目に読み込まれたtは21である。sumの値は0から21へと更新されて、cntの値は0から1へと更新される。
Note
その後、変数retryに0が読み込まれるため、ループ本体が再び実行される。
繰り返しの2回目を考えよう。整数値7をtに読み込んだ後に、再び
が実行されるので、sumは21から28に更新されて、cntは1から2へと更新される。
このようにして、読込みと加算が繰り返され、二つの変数は、次の値となる。
- 変数sum:キーボードから読み込んだtを合計した値。
- 変数cnt:読み込んだ数値の個数。
do文が終了すると、合計sumと、平均(double)sum / cntを表示する。
Note
平均を求める式の(double)は、double型に変換するためのキャストである。
演習 4-1
読み込んだ整数値の符号を判定するList 3-9(p.52)を、入力・表示を好きなだけ繰り返せるように変更したプログラムを作成せよ。
演習 4-2
右に示すように、二つの整数値を読み込んで、小さいほうの数以上で大きいほうの数以下の全整数を加えた値を表示するプログラムを作成せよ。
さて、整数値の合計と個数を更新する箇所は、次のようになっている。
このコードをスッキリさせたのが、List 4-4のプログラムである。二つの演算子+=と++が初登場である。それぞれを学習していこう。
Note
変更部のみを示している(プログラムの動作はList 4-3と同じである)。
複合代入演算子
まずは、①の+=演算子である。この演算子は、Fig.4-6に示すように、加算+と代入=の二つの演算をこなす、一人二役の演算子である。
【Fig.4-6】複合代入演算子による加算
『sumにtを加えた値をsumに代入』が、『sumにtを加える』と表現できる。
その+=演算子は、複合代入演算子(compound assignment operator)と呼ばれる演算子である。Table 4-2に示すように、+=を含めて全部で10個の複合代入演算子がある。
【Table 4-2】複合代入演算子
演算子 | 記法 | 説明 |
---|---|---|
複合代入演算子 | a @= b | 基本的にはa同じ(ただしaの評価は1度のみ行われる)。 |
@=は次のいずれか:*= /= %= += -= <<= >>= &= ^= |= |
演算子*、/、%、+、-、<<、>>、&、^、|に対しては、式a @= bは、式a = a @ bとほぼ同じ働きをする、と理解しておこう。
重要
+=などの複合代入演算子を使うと、プログラムがシンプルで読みやすくなる。
注意
複合代入演算子を使う際は、演算の優先順位に注意せよ。例えば x *= 2 + 3
は x = x * (2 + 3)
と同じであり、x = x * 2 + 3
とは異なる。
Note
たとえば、次のように利用する。
後置増分演算子と後置減分演算子
次は、②の++である。これは、後置増分演算子(postfixed increment operator)と呼ばれる演算子である。Table 4-3に示すように、式a++は、オペランドの値を一つだけ増やす。
値を一つだけ増やすことを『インクリメントする』というので、覚えよう。
【Table 4-3】後置増分演算子と後置減分演算子
演算子 | 記法 | 説明 |
---|---|---|
後置増分演算子 | a++ | aの値を一つだけ増やす(式全体を評価すると、増分前の値となる)。 |
後置減分演算子 | a-- | aの値を一つだけ減らす(式全体を評価すると、減分前の値となる)。 |
この演算子を使うことで、『cntに1を加えた値をcntに代入する』が、『cntをインクリメントする(値を1増やす)』と表現できる。
なお、オペランドの値を逆に一つだけ減らす(『デクリメントする』という)のが、後置減分演算子(postfixed decrement operator)と呼ばれる--演算子である。
二つの演算子の働きをまとめると、Fig.4-7のようになる。
【Fig.4-7】後置増分演算子と後置減分演算子
重要
後置増分演算子++と後置減分演算子--を利用すると、プログラムはシンプルで読みやすくなる。
なお、後置増分演算子と後置減分演算子の"後置"は、演算子をオペランドの後ろに置くことによるネーミングである。
Note
++と--をオペランドの前に置く"前置"の演算子もある(p.88で学習する)。
複合代入演算子と後置増分/減分演算子は、数学で使わない演算子であるので、難しく感じられるかもしれない。しかし、慣れてしまえば、便利で簡単である。
Note
今後のプログラムでも、頻繁に使っていく。
4-2 while文
do文とは異なり、繰り返しを継続するかどうかの判定を、ループ本体の実行後ではなく、実行前に行うのが、本節で学習するwhile文である。
while文
List 4-5に示すのは、整数値を読み込んで、その値を0までカウントダウンしながら表示するプログラムである。
// 読み込んだ整数値を0までカウントダウン(その1)
#include <stdio.h>
int main(void)
{
int no;
printf("正の整数を入力せよ:");
scanf("%d", &no);
while (no >= 0) {
printf("%d ", no);
no--; // noの値をデクリメント
}
printf("\n"); // 改行
return 0;
}
プログラムの流れを繰り返す水色の部分が、本節で学習するwhile文(while statement)である(前節のdo文ではない)。
do文で使われるwhileが先頭に位置しており、構文がFig.4-8のようになっていることは、プログラムから分かるであろう。
【Fig.4-8】while文の構文図
さて、このwhile文は、( )の中に置かれた式(制御式)を評価した値が真である(0でない)限り、文を繰り返し実行する。すなわち、プログラムの流れを、Fig.4-9のように制御する。
【Fig.4-9】while文のプログラムの流れ
Note
継続条件である制御式の評価を最初に行う点が、do文とは、まったく異なる。
それでは、Fig.4-10を見ながら、プログラムの流れを追っていこう(実行例①のように、変数noに5が入力されているものとする)。
【Fig.4-10】noの値の変化
まず制御式no >= 0が評価されて1が得られる。非0である1は真であるから、ループ本体が実行される。
ループ本体の複合文では、最初に
が実行されて、画面上に「5」と表示される(5に続いて空白文字が表示される)。
その後で実行されるのが、次の文である。
後置減分演算子--の働きによって、変数noはデクリメントされて、その値が5から4へと更新される。
これでループ本体の実行は終了し、プログラムの流れはwhile文の先頭へと戻る。
繰り返しの継続条件の判定=制御式の評価が行われる(2回目である)。
制御式no >= 0の評価で1が得られるため、ループ本体が再び実行される。そのループ本体では、画面に「4」と表示されて、noが4から3へとデクリメントされる。
この処理の繰り返しによって、変数noの表示とカウントダウンが行われていく。
6回目の繰り返しのときのnoの値は0である。その値0が表示された後で、演算子--の働きによってnoの値は-1になる。
その後で行われる、繰り返しを続けるかどうかの7回目の判定では、継続条件no >= 0が成立しない(制御式の評価値が0となる)ので、while文の繰り返しが終了する。
Note
noが0のときにno-- > 0が評価された際にnoのデクリメントは行われるので、while文が終了したときのnoの値は-1である。
実行例③では、変数noに-5が読み込まれている。制御式no >= 0を1回目に評価したときに得られるのは0すなわち偽である。そのため、ループ本体は一度も実行されることがない。すなわち、while文は実質的に素通りされる。
Note
while文の後ろに置かれた『printf("\n");』は、while文とは無関係に実行される。そのため、実行例③のように、変数noの値が負であれば、改行文字だけが出力される。
注意
do文のループ本体は少なくとも1回は実行されるのに対し、while文のループ本体は1回も実行されないことがある。この違いを理解し、適切なループ構造を選択することが重要。
演習 4-3
負の値を読み込んだときに改行文字を出力しないように、List 4-5のプログラムを書き換えよ。
減分演算子を用いた手短な表現
後置減分演算子--の特性をうまく利用すると、カウントダウンのプログラムは、もっと短く簡潔に実現できる。List 4-6に示すのが、そのプログラムである。
// 読み込んだ整数値を0までカウントダウン(その2)
#include <stdio.h>
int main(void)
{
int no;
printf("正の整数を入力せよ:");
scanf("%d", &no);
while (no >= 0)
printf("%d ", no--); // noの値を表示した後にデクリメント
printf("\n"); // 改行
return 0;
}
後置増分演算子と後置減分演算子の概要を示したTable 4-3(p.81)を、もう一度読み直してみよう。後置減分演算子a--は、次のように解説されている。
aの値を一つだけ減らす(式全体を評価すると、減分前の値となる)。
すなわち、式a--の評価で得られるのが、デクリメント前の値であることが分かる。
Fig.4-11に示すように、変数noの値が11であれば、式no--を評価して得られるのは、デクリメント前の11である(この値が得られた後に、デクリメントが行われる)。
【Fig.4-11】後置減分演算式の評価
本プログラムのprintf("%d ", no--)が行うことを分割して説明すると、
- noの値を取り出して表示する。
- noの値をデクリメントする。
となる。すなわち、noの値を表示した直後にデクリメントが行われる、というわけである。
演習 4-4
List 4-6のプログラムを、次のように書き換えたプログラムを作成せよ。 - 0ではなく1までカウントダウンする。 - 入力された値が0以下であるときには、改行を行わない。
カウントアップ
前ページのプログラムとは逆に、0から始めて、読み込んだ整数値までカウントアップすることを考えよう。そのプログラムをList 4-7に示す。
// 読み込んだ正の整数値までカウントアップ
#include <stdio.h>
int main(void)
{
int no;
printf("正の整数を入力せよ:");
scanf("%d", &no);
int i = 0;
while (i <= no)
printf("%d ", i++); // iの値を表示した後にインクリメント
printf("\n"); // 改行
return 0;
}
カウントダウンのプログラムと大きく異なるのは、変数iが新しく導入されていることである。
そのiの値は、後置増分演算子++の働きによって、0、1、2、…と増えていく。
Note
ループ本体が最初に実行されるときは、まずiの値0を表示して、その直後にインクリメントすることで値を1に更新する(2回目は、変数iの値1を表示した直後にインクリメントすることで2に更新する)。
実行例の場合、繰り返し12回目での変数iの値は、noと同じ12である。その表示を行った直後にiの値がインクリメントされて13になるため、while文の繰り返しが終了する。
Note
表示は12までであるが、変数iの最終的な値は13であることに注意しよう。
ポイント
繰り返し処理でカウンタ変数を使う場合は、初期値・終了条件・増減方法の3点を明確にすることが重要。これらがわかりやすく書かれていると、プログラムの意図が伝わりやすくなる。
演習 4-5
List 4-7のプログラムを、次のように書き換えたプログラムを作成せよ。 - 0からではなく1からのカウントアップを行う。 - 入力された値が0以下であるときには、改行を行わない。
演習 4-6
右に示すように、読み込まれた整数以下である正の偶数を順に表示するプログラムを作成せよ。
演習 4-7
右に示すように、読み込まれた整数以下である正の2のべき乗の数を順に表示するプログラムを作成せよ。
文字定数とputchar関数
整数値を読み込んで、その個数だけアスタリスク記号*を横に連続して並べて表示するプログラムを作ろう。それが、List 4-8に示すプログラムである。
// 読み込んだ整数の個数だけ*を連続表示
#include <stdio.h>
int main(void)
{
int no;
printf("正の整数:");
scanf("%d", &no);
while (no-- > 0)
putchar('*');
putchar('\n');
return 0;
}
実行例①のように、読み込んだnoが15であるとして、プログラムの流れを考えていこう。
まず最初に、while文の制御式no-- > 0が評価される。--は後置減分演算子であるから、変数noの値15が0より大きいかどうかが評価され、判定が成立することが確認された直後に、noの値がデクリメントされて14となる。
つまり、この制御式no-- > 0の判定は、次のように行われる。
noが0より大きいかどうかを判定して、判定が終わった直後にnoをデクリメントする。
さて、noの値は、制御式を通過するたびに15 ⇒ 14 ⇒ 13…とデクリメントされていく。
制御式no-- > 0の評価値が偽になるのは、変数noの値が0のときである。これで、全部でno回の繰り返しが行われる。
Note
noが0のときにno-- > 0が評価された際にnoのデクリメントは行われるので、while文が終了したときのnoの値は-1である。
文字定数
while文のループ本体では『putchar('*');』が実行され、終了後に『putchar('\n');』が実行されている。
ここで使われている'*'や'\n'のように、単一引用符'で文字を囲んだ式は、文字定数(character constant)と呼ばれ、その型はint型である。
重要
文字は、単一引用符で文字を囲んだ'*'形式の、int型の文字定数で表す。
文字定数は、これまで使ってきた文字列リテラルとは、まったく異なる。
- 文字定数''… 単一の文字を表す(この場合は、文字)。
- 文字列リテラル""… 文字の並びを表す(この場合は、たまたま1個の文字)。
Note
文法上、'ab'のように、''の中に複数の文字を入れることも可能であるが、使うべきではない。というのも、その解釈が処理系に依存するからである。
注意
文字定数と文字列リテラルを混同しないように気をつけましょう。特に、putchar関数には文字定数を、printf関数には文字列リテラルを渡す必要がある。
putchar関数
単一文字の表示のために使っているのが、putchar関数である。( )の中には、表示すべき文字を実引数として与える。
本プログラムでは、ループ本体でno個の'*'を表示している。そして、while文終了後に改行するために、改行文字'\n'を出力している。
重要
putchar関数を利用すると、単一文字の表示が行える。
なお、次に示すコードは、誤りである("chap04/putcharx.c")。
putchar("A"); // エラー:putcharに渡すのは文字。 正しくはputchar('A');
printf('A'); // エラー:printf に渡すのは文字列。正しくはprintf("A");
do文とwhile文
実行例②や実行例③のように、0や負の値を入力すると、while文が実質的に素通りされて、改行だけが行われる。すなわち、アスタリスク記号'*'が1個も表示されることなく、改行文字だけが出力される(これまでのプログラムも同様である)。
このように、while文の制御式を1回目に評価した際に得られるのが偽(すなわち0)であれば、ループ本体は1回も実行されない。
これが、do文とは大きく異なるwhile文の特徴である。
重要
do文のループ本体は少なくとも1回は実行されるのに対し、while文のループ本体は1回も実行されないことがある。
繰り返しの継続条件の判定のタイミングは、do文とwhile文とでまったく異なる。
- do文 … 後判定繰り返し:ループ本体を実行した後に継続条件の判定を行う。
- while文 … 前判定繰り返し:ループ本体を実行する前に継続条件の判定を行う。
Note
次節で学習するfor文は、前判定繰り返しである。
演習 4-8
読み込んだ値が1未満であれば改行文字を出力しないようにList 4-8を書き換えたプログラムを作成せよ。
前置増分演算子と前置減分演算子
List 4-9のプログラムを考えよう。最初に変数numに整数値を読み込み、そのnum個の整数を次々と読み込んで、合計値と平均値を表示するプログラムである。
// 指示された個数だけ整数を読み込んで合計値と平均値を表示
#include <stdio.h>
int main(void)
{
int num;
printf("整数は何個:");
scanf("%d", &num);
int i = 0;
int sum = 0; // 合計値
while (i < num) {
int tmp;
printf("No.%d:", ++i); // iの値をインクリメントした後に表示
scanf("%d", &tmp);
sum += tmp;
}
printf("合計値:%d\n", sum);
printf("平均値:%.2f\n", (double)sum / num);
return 0;
}
①で使っている++は、前置増分演算子(prefixed increment operator)であり、これとペアになるのが前置減分演算子(prefixed decrement operator)である(Table 4-4)。
【Table 4-4】前置増分演算子と前置減分演算子
演算子 | 記法 | 説明 |
---|---|---|
前置増分演算子 | ++a | aの値を一つだけ増やす(式全体を評価すると、増分後の値となる)。 |
前置減分演算子 | --a | aの値を一つだけ減らす(式全体を評価すると、減分後の値となる)。 |
前置増分演算子は、インクリメントを行う点では、後置増分演算子と同じであるが、そのタイミングが異なる。それを対比したのが、Fig.4-12である。
【Fig.4-12】増分演算式の評価
この図から、①の表示が、次のように行われることが分かる。
- iの値をインクリメントする。
- iの値を取り出して表示する。
すなわち、iの値を表示する直前にインクリメントするわけである。そのため、繰り返しの1回目では、0をインクリメントした後の1が「No.1:」と表示される。
前置と後置の、増分演算子++と減分演算子--をまとめると、次のようになる。
重要
後置(前置)の増分演算子++/減分演算子--を適用した式の評価で得られるのは、インクリメント/デクリメントを行う前(後)の値である。
考えてみよう
i = 5; j = ++i + i++;
という式の結果はどうなるのか。このような複雑な式は可読性が低いため、避けるべきである。同一文において同一変数に対し複数回の増減演算を行うことは推奨されない。
do文の表記
do文とwhile文の両方にキーワードwhileが含まれている。そのため、プログラム中のwhileが、『do文の一部』なのか『while文の一部』なのかが見分けづらくなりがちである。
その対策を、Fig.4-13で考えよう。
【Fig.4-13】do文とwhile文
図aは、『do文のwhile』の真下に、『while文のwhile』が位置している。
一方、do文のループ本体を{}で囲んで複合文にした図bでは、行の先頭が}であるかどうかで、do文とwhile文の見分けが付くようになっている。
重要
do文のループ本体は、たとえ単一の文であっても、{}で囲んで複合文にするスタイルを採用すれば、プログラムの読みやすさが向上する。
本書は、このスタイルで統一する。
コーディングのコツ
プログラムの可読性を高めるためには、一貫したコーディングスタイルを保つことが重要。インデントや波括弧の配置など、統一されたルールに従うことで、他の開発者(そして将来の自分)がコードを理解しやすくなる。
整数値を逆順に表示
List 4-10は、読み込んだ正の整数値の桁の並びを《反転して》表示するプログラムである。
たとえば、変数noに1963が入力されると、3691と表示する。
Note
最初のdo文は、読み込むのを正値に制限するための繰り返し文である。
// 読み込んだ正の整数値を逆順に表示
#include <stdio.h>
int main(void)
{
int no;
do {
printf("正の整数を入力せよ:");
scanf("%d", &no);
if (no <= 0)
puts("\a正でない数を入力しないでください。");
} while (no <= 0);
// noは0より大きくなっている
printf("その数を逆から読むと");
while (no > 0) {
printf("%d", no % 10); // 最下位の桁の値を表示
no /= 10; // 右に1桁ずらす
}
puts("です。");
return 0;
}
Fig.4-14を見ながら、while文を理解していこう。
【Fig.4-14】10進数を逆順に表示
ループ本体で行うのは、次の二つのことである。
- noの最下位桁の表示
noの最下位桁の値であるno % 10を表示する。
たとえば、noが1963であれば、表示するのは、10で割った剰余の3である。
- noを10で割る
表示後の『no /= 10;』が行うのは、『noを10で割る』ことである(演算子/=は、p.80で学習した複合代入演算子である)。
たとえば、noが1963であれば、演算後のnoは、10で割った196になる。
変数noの最下位桁を弾き出して、それ以外の桁を右に1桁ずらすことが分かるであろう。
Note
繰り返しの2回目では、変数noの値は196である。10で割った剰余6を表示した後に、noを10で割って19にする。
以上の処理を繰り返して、noの値が0になるとwhile文は終了する。
複合代入演算子を利用した二つ目のプログラムであった。複合代入演算子には、次のメリットがある。
- 行うべき演算を簡潔に表せる
『noを10で割った商をnoに代入する』よりも、『noを10で割る』のほうが、簡潔であるだけでなく、私たち人間にとっても自然に受け入れられる表現である。
- 左辺の変数名を書くのが1回ですむ
変数名が長い場合や、(後の章で学習する)配列や構造体を用いた複雑な式では、タイプミスの可能性が少なくなり、プログラムも読みやすくなる。
- 左辺の評価が1回限りである
複合代入演算子を利用する最大のメリットは、左辺の評価が行われるのが1回のみであることである。
これらのメリットは、コードが複雑になるほど大きくなる。たとえば、
では、iのインクリメントは1回限りである。もし複合代入演算子を使わなければ、次のように、文を二つに分けるとともに、長い式を2回も書かなければならない。
Note
ここで使っている演算子[ ]は第5章で学習し、演算子.は第12章で学習する。
演習 4-9
読み込んだ値の個数だけ+と-を交互に表示するプログラムを作成せよ。 なお、0以下の整数が入力された場合は、何も表示しないこと。
演習 4-10
読み込んだ整数値の個数だけ*を縦に連続して表示するプログラムを作成せよ。なお、0以下の整数が入力された場合は、何も表示しないこと。
演習 4-11
List 4-10のプログラムを、結果の出力時に読み込んだ値も表示するように書き換えよ。
演習 4-12
正の整数値を読み込んで、その桁数を表示するプログラムを作成せよ。 ※ヒント:List 4-10のwhile文の繰り返しの回数は、noの桁数と一致する。
break文とcontinue文
次の問題を考えよう。
整数値を次々と読み込んで、正の値のみを加算する。ただし、-9999が入力されたら読込みを中断して、合計を表示する。
そのプログラムがList 4-11である。
// 整数を次々と読み込んで正の整数値のみの合計を求める
#include <stdio.h>
int main(void)
{
puts("正の整数値を加算します(終了は-9999)。");
int sum = 0;
while (1) {
int no;
printf("整数値:");
scanf("%d", &no);
if (no == -9999)
break;
else if (no <= 0)
continue;
sum += no;
}
printf("正の整数の合計は%dです。", sum);
return 0;
}
無限ループ
while文の制御式に着目しよう。ただ1とだけ書かれている。整数値1は真とみなされるので、このwhile文の繰り返しは、永遠に行われることになる。
このような、永遠に行われる繰り返しは、無限ループと呼ばれる。
さて、そのwhile文のループ本体内のif文では、次のことを行っている。
- noが-9999であれば、break文を実行 :読込みを終了する
- noが0以下であれば、continue文を実行:加算を行わない
それぞれで使われている、break文とcontinue文を学習していこう。
break文
①では、変数noに読み込んだ値が-9999のときに、break文が実行されている。
Note
switch文の中でbreak文が実行されると、プログラムの流れがswitch文を抜け出ることは、前章で学習した(p.67)。
do文、while文、(次節で学習する)for文といった、繰り返しを行う文のループ本体の中でbreak文が実行されると、プログラムの流れは繰り返し文を抜け出す(Fig.4-15)。
【Fig.4-15】break文とcontinue文の働き
重要
繰り返しを行う文のループ本体の中でbreak文が実行されると、プログラムの流れは、その繰り返しを抜け出る(繰り返しを強制的に中断する)。
危険
無限ループ内でbreak文を忘れると、プログラムが終了しない「無限ループ」状態になる。必ず脱出条件を設けるようにせよ。
変数noに読み込んだ値が-9999のときにbreak文が実行される結果、無限ループであるはずのwhile文の繰り返しが、強制的に中断されることが分かった。
continue文
②では、Fig.4-16に示す構文図をもつcontinue文が実行されている。
【Fig.4-16】continue文の構文図
このcontinue文は、break文と対照的な働きをする文である。
ループ本体の中でcontinue文が実行されると、ループ本体の残りの部分の実行がスキップされて、プログラムの流れは、制御式に飛ぶ。
重要
繰り返しを行う文のループ本体の中でcontinue文が実行されると、ループ本体の残りの部分の実行がスキップされて、プログラムの流れは制御式に飛ぶ。
具体例
continueの使用例:「1から100までの数字のうち、3の倍数だけをスキップして表示する」という処理では、if (i % 3 == 0) continue;
とすることで3の倍数をスキップできる。
変数noに読み込んだ値が0以下のときはcontinue文が実行されるので、ループ本体の残りの部分『sum += no;』がスキップされる結果、sumへの加算が行われなくなる。
よくある間違い
繰り返し処理で見落としがちなエラー: - カウンタの初期化忘れ - 終了条件の設定ミス(「<」と「<=」の混同など) - 無限ループ内の脱出条件の欠如 - continueの後に重要な処理を書いてしまう
演習問題
演習問題4-1:交互記号表示
問題の説明
正の整数値nを入力し、n個の「+」と「-」を交互に表示するプログラムを作成してください。入力が0以下の場合は「正の整数を入力してください」と表示します。
期待される結果
入力例1:
出力例1:入力例2:
出力例2:ヒント
- 繰り返し回数を数えるためのカウンタ変数を使いましょう
- カウンタ変数が偶数か奇数かで表示する記号を切り替えます
- 奇数偶数の判定には剰余演算子「%」を使います(i % 2 == 0 で偶数かどうかを判定)
- putchar関数を使って1文字ずつ出力すると効率的です
演習問題4-2:桁数カウント
問題の説明
正の整数値を入力し、その桁数を表示するプログラムを作成してください。入力値が0以下の場合は、再入力を求めるようにしてください。
期待される結果
入力例1:
出力例1:入力例2:
出力例2:ヒント
- do-while文を使うと、「最低1回は実行してから条件をチェックする」処理ができます
- 入力値の検証にはdo-while文が適しています
- 整数を10で割り続けると、桁数を数えることができます
- 数値の計算中に元の値を残しておきたい場合は、別の変数にコピーしましょう
演習問題4-3:階段描画
問題の説明
正の整数nを入力し、高さnの階段を「*」記号で描画するプログラムを作成してください。入力値が0以下の場合は再入力を求めるようにしてください。
期待される結果
入力例1:
出力例1:入力例2:
出力例2:ヒント
- 二重ループを使う必要があります
- 外側のループは行数(1〜n)を制御します
- 内側のループは各行の「*」の数(行番号と同じ数)を制御します
- 各行の出力が終わったら改行を忘れないようにしましょう
- do-while文を使って入力の妥当性チェックを行います
演習問題4-4:九九の表の表示
問題の説明
九九の表(1から9までの掛け算の表)を表示するプログラムを作成してください。
期待される結果
1 2 3 4 5 6 7 8 9
2 4 6 8 10 12 14 16 18
3 6 9 12 15 18 21 24 27
4 8 12 16 20 24 28 32 36
5 10 15 20 25 30 35 40 45
6 12 18 24 30 36 42 48 54
7 14 21 28 35 42 49 56 63
8 16 24 32 40 48 56 64 72
9 18 27 36 45 54 63 72 81
ヒント
- 二重ループを使用して、外側のループで段数(行)、内側のループで掛ける数(列)を制御します
- 各数値を表示する際には、桁をそろえるために書式指定子「%3d」を使うと見やすくなります
- 各行の終わりに改行文字を出力するのを忘れないようにしましょう
- 九九表は9×9の正方形になることに注意してください