第4章 プログラムの流れの繰返し

テキスト『新・明解C言語 入門編』第4章の内容に関する補足説明を提供します.

4-1 do 文

このセクション(4-1)の冒頭に書いてあるように, C 言語では処理の繰り返しのための文が3種類用意されています. すなわち,do 文(4-1 節),while 文(4-2 節), for 文(4-3節)です. テキストではこれらのうち do 文から学習しますが, 実はこれは3種類の文の中で使用頻度が最も低いものです.

do 文

do 文でのループ本体(loop body)は, 必ず1回は実行されます.Fig. 4-2(p.73)に示されているように, ループ本体を一度実行してから, 制御式の最初の評価が行われるからです. これが do 文の特徴です.

後で学習する while 文および for 文で実現できず, do 文でしか書けない処理というものはありません. do 文を使用しないプログラマも多いようです. しかし,ループ本体を必ず一度は実行するなら, 積極的に do 文を使ってよいと思います.

do 文の最後,while に続く制御式の後には,セミコロンが必要です. これは忘れやすいので注意してください. Fig. 4-1 に示されているように,while に続く制御式までが do 文なのです. do 文の終わりを示すために,セミコロンをつけると考えてください. テキストの List 4-1 では


while (retry == 0);

となっています. セミコロンがいつ必要になるかを判断するルールについては, 以下の書籍の第2部 Part 1 に説明があります(p.102). これは「日経ソフトウェア」という雑誌の連載記事をまとめた書籍です.

川俣晶 (2010) 実践 C言語プログラミング 日経BP社

List 4-1 の do 文の制御式で,変数 retry の値と, 制御式 retry == 0 が返す値を混同しないようにしてください. retry の値が 0 のとき,この制御式の値が 1 になります. すぐ後(p.75)で学習する 論理否定演算子(logical negation operator)を使うなら, この制御式は


while (!retry);

と書くこともできます.

複合文内での宣言

第3章ですでに学習したように(p.58), 複合文(compound statement)の中で変数の宣言を行う場合には, ブロックの先頭で行ってください. C 言語の最近の規格(C99 および C11)では, ブロックの先頭でなくても変数の宣言が許されるようになりました. しかし,変数の宣言をブロックの先頭にまとめた方が, 宣言忘れなどのミスを発見しやすいと思います.

一定範囲の値の読込み

許可されていない値が入力されたときには, List 4-10(p.88)での do 文で行っているように, なぜその入力が不適切なのかを知らせた方がより親切です.

論理否定演算子とド・モルガンの法則

論理否定演算子(logical negation operator)は, 単項算術演算子(unary arithmetic operator) のひとつでした(p.26)

ド・モルガンの法則(De Morgan's theorem)は, 高校数学での集合の単元で学習するので,覚えている人も多いと思います. そのときには,


not (A or B) = (not A) and (not B)
not (A and B) = (not A) or (not B)

という形で学習しているはずです. これをテキストで述べられている等式に書き換えてみましょう. 等式


 not (A or B) = (not A) and (not B)

を,C 言語の演算子を使った式に書き換えると,


!(A || B) = !A && !B

となります.ここで,


A = !x, B = !y

とおくと,


!(!x || !y) = !(!x) && !(!y)

という等式が得られます.


!(!x) = x, !(!y) = y

であることに注意すると,けっきょく,


!(!x || !y) = x && y

という等式が得られます.テキストでのもうひとつの等式も, 同じようにして得ることができます.

式変形だけでは納得できないかもしれないので, ベン図を使った説明(証明としては不完全)を述べておきます. ベン図はおそらく知っているでしょう. 集合の包含関係を表すのに使われる,下のような図です.

Venn Diagram

このベン図において,x && y の領域は, 下図のように x の円と y の円の共通部分になります,

Venn Diagram x && y

この領域が,!(!x || !y) の領域と同じになることを示します. 一番外側の否定を外して,(!x || !y) はどの領域になるかを考えます. まず,!x は,x の円の外側なので,下図の領域です.

Venn Diagram !x

次に,!y は,y の円の外側なので,下図の領域です.

Venn Diagram !y

(!x || !y) の領域は,!x の領域と !y の領域を合わせて, 下図のようになります.

Venn Diagram (!x || !y)

すると,!(!x || !y) の領域は,上図で色を塗られていない部分ですから, x && y の領域と同じになることがわかります. x と y の包含関係には, このベン図とは別のパターン(たとえば,x が y を完全に含む)もありますから, 以上の説明だけでは証明としては不完全です. しかし,法則を納得するにはこれで十分でしょう.

複数の整数値の合計と平均を求める

このセクションについての補足説明は現在のところありません.

複合代入演算子

これまで使ってきた sum = sum + t という代入式と, 複合代入演算子(compound assignment operator)を用いた sum += t という代入式は, 同じ結果をもたらします.いずれの式も, 評価の結果は左オペランド(sum)の型と値になります(テキスト p.120 参照). テキスト p.78 では,これら2つの代入式は「ほぼ」同じと書かれています. 完全に同じでないのは, sum = sum + t という代入式では sum の評価が2回であるのに対して, sum += t では1回という違いがあるからです(Table 4-2 参照). ただし,コンパイラは最適化を行うので, 実際には完全に同じ実行ファイルが生成されるでしょう (以下の書籍の 3.2.2 節を参照).

株式会社システム計画研究所(編) (2001) C言語によるプログラミング 基礎編(第2版) オーム社

後置増分演算子と後置減分演算子

後置増分演算子(postfixed increment operator) を用いた式 a++ を評価すると,増分前の値になります(Table 4-3). 後置減分演算子(postfixed decrement operator) についても同様です. プログラム例が示されていないので, これはどういうことなのか,わかりにくいと思います. List 4-6 でこの性質を使います.とりあえず, a = a + 1 という代入式を書く代わりに, a++ と書くことができるということを理解してください.

4-2 while 文

while 文

Fig. 4-8 に示されているように,while 文は何らかの文で終わります, たとえば,List 4-6 では,


while (no >= 0)
    printf("%d ", no--);

というように,printf の文で終わっています. 複数の文を並べる必要があるときは複合文を使います. たとえば,List 4-5 では,


while (no >= 0) {
    printf("%d ", no);
    no--;
}

としています.

ここで,while 文の中に埋め込まれている文ではなく, while 文そのものの終わりにはセミコロンは必要ないのだろうかと, 疑問に思うかもしれません. do 文の終わりにはセミコロンをつけましたし, 文の終わりにはセミコロンをつけるというのが原則だったはずです.

List 4-5 や List 4-6 からわかるように, while 文そのものの終わりを示すセミコロンは不要です. つけてしまうと,セミコロンが2重になるか, 複合文を示す { } のあとにセミコロンをつけることになってしまって, おかしいですよね.

文の終わりにはセミコロンをつけるという原則からすると, while 文はこの原則から外れた例外ということになります. しかし,while 文ではこの原則に従っていなくても, どこが while 文の終わりであるかは明確にわかります. だから,while 文そのものの終わりにはセミコロンは不要なのです.

セミコロンがいつ必要になるかを判断するルールについては, 例外を含め, 以下の書籍の第2部 Part 1 に説明があります(p.102). これは「日経ソフトウェア」という雑誌の連載記事をまとめた書籍です.

川俣晶 (2010) 実践 C言語プログラミング 日経BP社

減分演算子を用いた手短な表現

テキストで説明されているように, 後置増分演算子後置減分演算子 の特性をうまくを使うと,プログラムを短くすることができます. List 4-6 は List 4-5 よりも1行短くなっています. しかし,プログラムを短くした代償に, printf 関数で表示される数値が何になるのか, 明快さが失われています. プログラムを短くすることよりも, わかりやすさを優先すべきという考えに立てば, List 4-6 よりも List 4-5 の方がよいと言えます.

コンピュータのメモリが高価で,容量も小さかった時代には, プログラムを短くすることは重要でした. しかし現在では,一般にわかりやすさの方が優先されます. 以下の書籍, Oualline (1998) の 5.12 節(p.69-70)や, 前橋 (2001) の 2-2-4 節(p.87-88)を参照してください.

S. Oualline (1998) C実践プログラミング(第3版) オライリー・ジャパン

前橋和弥 (2001) C言語 体当たり学習徹底入門 技術評論社

カウントアップ

List 4-7 では,while 文の直前で変数 i に値 0 を代入しています. もちろん,最初に変数 i を宣言したときに, 同時に 0 で初期化しておくことができます. それを後回しにしたのは, 4-3 節で学習する for 文との比較(Fig. 4-17)のためです.

演習 4-6 では,偶数を昇順に表示しています. 偶数を降順に表示するプログラムも書いてみてください. たとえば,19 が入力されたら, 18 16 ... 2 と表示します.

一定回数の繰り返し

List 4-8 で, 改行の出力を行う文 putchar('\n'); は,while 文の外側にあります. while 文とくっついていますが,while ループの一部ではありません.


while (no-- > 0)
    putchar('*');

putchar('\n');

というように,while 文の後を1行あけた方がわかりやすいです. あるいは,複合文を使って,


while (no-- > 0) {
    putchar('*');
}
putchar('\n');

とします.

List 4-8 を実行するときには,入力する数は 15 でなく, もっと小さい数にした方がよいでしょう. プログラムが意図したとおりに動いているかを確認するとき, * を 15 も数えるのは大変なので.

List 4-8 の while のループを抜けたときに, 変数 no の値は -1 になっています. これを確かめるには, printf 関数を使って no の値を表示させればよいでしょう. ついでに, while ループの中での no の値の更新も確かめてみましょう. List 4-8 を次のように書きかえます.

List 4-8 に printf 関数を挿入

このプログラムを実行すると以下のようになります.

List 4-8 の実行結果

変数 no の値の更新に注目すると,List 4-8 の動作が理解できます.

変数 no の値を 5 と入力しました.これは 0 より大きいので, while 文の制御式 no-- > 0 の値は 1(真)となります. no の値はデクリメントされて 4 になります. 制御式の左辺 no-- の評価によって得られるのは, デクリメント前の値(ここでは 5)であることに注意してください. 制御式は真なので,while ループ本体が実行されます. putchar 関数によって * をひとつ出力します.このときには, no の値は 4 です.

ループの最後に到達すると, while 文の先頭に戻って,制御式が再び評価されます. no の値は 4 なので,制御式の値は 1 となります. no の値はデクリメントされて 3 になります. putchar 関数によって * をひとつ出力します. このように処理を繰り返して,* を出力していきます.

変数 no の値が 0 のときに制御式が偽となり, ループ本体を実行せずに次に進みます. ループ本体は実行されなくても no のデクリメントは行われるので, while ループを抜けたときの no の値は -1 です.

プログラムの動作を理解するために, プログラム実行中の変数の状態を, printf 関数を用いて確認しました.この方法は, プログラムのバグを修正するためによく用いられます. プログラムがうまく動作しないとき, printf 関数を用いて変数の状態を確認することを, printf デバッグと呼びます.

文字定数と putchar 関数

コンピュータの内部で文字を扱うために, 文字にはそれぞれ 文字コード(character code) という非負の整数値が割り振られています. 文字コードの体系にはいくつかの種類があります. テキスト p.232 には, JIS コードのコード表が示されています. この文字コード体系では, たとえば,大文字の A は10進数の 65 です. Mac OS では ASCII コード という文字コードが用いられています.

C言語では,文字コードを利用して, 文字を数値と同じように扱います.文字定数の型は int 型です. 数値を変数に代入(あるいは,変数を特定の数値で初期化)できたように, 文字を変数に代入(あるいは,変数を特定の文字で初期化)できます. たとえば,変数 x を文字定数 'A' で初期化し, 変数 x の内容を printf 関数で表示させるには, 次のようなプログラムを書きます.


int main(void)
{
    int x = 'A';
    printf("x の中身は %c です。\n", x);
    return 0;
}

このプログラムでは,int 型の変数 x は A の文字コードで初期化されます. ASCII コードあるいは JIS コードなら 65 です. printf 関数の書式文字列中で使用している変換指定子 c は, 整数値を文字コードとして解釈し, その文字コードに対応する文字を出力します(13-3 節にある, printf 関数の仕様を参照してください). プログラムを実行すると「x の中身は A です。」と出力されます.

文字コードは整数値ですから,その整数値を出力することもできます. たとえば,次のプログラムを実行してください.


int main(void)
{
    int x = 'A';
    printf("文字 %c の文字コードは %d です。\n", x, x);
    return 0;
}

ASCII コードあるいは JIS コードの場合, 「文字 A の文字コードは 65 です。」と出力されます.

putchar 関数の引数に文字コードを指定すると, その文字コードに対応する文字が出力されます.たとえば, putchar(65) とすると,ASCII コードあるいは JIS コードの場合, 文字 A が出力されます.

do 文と while 文

このセクションについての補足説明は現在のところありません.

前置増分演算子と前置減分演算子

このセクションについての補足説明は現在のところありません.

do 文の表記

このセクションについての補足説明は現在のところありません.

整数値を逆順に表示

List 4-10 の while ループで, 数値の最下位の桁を表示するのためにその数値を 10 で割った剰余を求めているのは, List 2-2(p.24)でも利用したテクニックです.

複合代入演算子のメリットの説明には, 少しおかしなところがひとつあります. 左辺の評価が1回限りであるというメリットの具体例で, 「i の値がインクリメントされるのは1回限りです」 と書かれていますが,インクリメントが1回であるのは, 複合代入演算子を使わない書き方でも同じです. ここでは,


computer.memory[vec[++i]] += 10;

では computer.memory[vec[++i]] の評価が1回であるのに対して,


++i;
computer.memory[vec[i]] = computer.memory[vec[i]] + 10;

では computer.memory[vec[i]] の評価が2回であると書くべきです. 前者では i の評価は1回,後者の第2文では i の評価は左辺と右辺で2回ですので, 「i の値が評価されるのは1回限りです」と書くなら適切です.

4-3 for 文

for 文は, カウンタの役割をするカウンタ変数を用意し, 決まった数だけ繰り返しを行う処理に向いています. この処理は while 文でも書けますが, for 文ではカウンタ変数の扱いを一か所(for の直後)にまとめて記述するため, よりわかりやすくなります.

for 文

List 4-11 の for 文で用いられている変数 i は, カウンタの役割をするので, カウンタ変数と呼ばれます. 最初は 0 で,繰り返しのたびにカウンタをひとつずつ進め, 変数 no の値を超えたところで繰り返しを止めます.

for 文の構文での,for (式1; 式2; 式3) という部分にある3つの式は, 不要ならばいずれも省略できます. 式は省略してもセミコロンは残すことに注意してください. 式2 を省略すると無限ループになります. 式2 を省略してしまうと,式1 と式3 もたいてい無意味になり, 省略されます. 無限ループはプログラムが終了しないことを意味するので恐ろしく思えますが, 実際にはしばしば用いられます. ループを抜け出す条件を if 文で書き, break 文でループを抜け出します. 無限ループではありませんが, break 文で for ループを抜け出す例が p.97 にあります.

Fig. 4-17 で for 文と while 文を「ほぼ」同じと書いています. わざわざ「ほぼ」としているのは, Column 4-1(p.101)で説明している continue 文を使った時の動作が少し異なるからでしょう. ループ本体で continue 文が使われたとき, while 文では継続条件の制御式(Fig. 4-17 での B)に進むのに対して, for 文ではカウンタ変数の更新(Fig. 4-17 での C)に進みます. この説明は,C 言語の「バイブル」である以下の書籍の, 3.5 節および 3.7 節に書かれています.

B. W. カーニハン & D. M. リッチー (1989) プログラミング言語C 第2版 共立出版

for 文による一定回数の繰返し

Fig. 4-18 に示されている while 文と for 文の違いは, これらの文で常に生じる一般的な違いではありません. List 4-12 にあわせて,List 4-8 を次のように書き直してみます. 変数の状態を確認するために,printf 関数を挿入しています.

List 4-8 を List 4-12 にあわせて改変

このプログラムの実行結果は次のようになります. List 4-12 と同様に,繰り返しにおいて no の値は変化しておらず, while ループを抜けたとき i の値は 6 です.

List4-12 にあわせて改変した List 4-8 の実行結果

偶数の列挙

このセクションについての補足説明は現在のところありません.

約数の列挙

このセクションについての補足説明は現在のところありません.

式文と式

while 文と同じく,for 文も何らかの文で終わります(Fig. 4-15 参照), for 文での for に続くかっこの後ろでは, まだ for 文は終わっていないのですから, セミコロンを置くことは誤りとなります. かっこの次に来る文の末尾には, この文の終わりを示すセミコロンが必要となります. while 文の場合と同じく, for 文そのものの終わりを示すセミコロンは不要です. つけてしまうと,セミコロンが2重になるか, 複合文を示す { } のあとにセミコロンをつけることになってしまって, おかしいですよね.

セミコロンがいつ必要になるかを判断するルールについては, 例外を含め,以下の書籍の第2部 Part 1 に説明があります(p.102). これは「日経ソフトウェア」という雑誌の連載記事をまとめた書籍です.

川俣晶 (2010) 実践 C言語プログラミング 日経BP社

繰返し文

このセクションについての補足説明は現在のところありません.

4-4 多重ループ

2重ループ

このセクションについての補足説明は現在のところありません.

図形の描画

List 4-18 および List 4-19 で描画する直角二等辺三角形では, 長さが等しい2辺は直角を構成している2辺です.

List 4-19 は少し複雑です.テキストの説明を参考にして, よく動作を追ってください. 描画の最終行(テキストの実行例では第5行)では, 空白文字を出力するための for 文の条件(j <= len - i) が満たされないので,空白は出力されません. 第4行の出力を終えて, 最終行の空白を出力する for 文の処理を開始する時点に注目します. テキストの実行例では, カウンタ変数 i の値は 5 にインクリメントされています. カウンタ変数 j の値は 1 に初期化されます. 変数 len の値は 5 ですので,len - i の値は 0 となり, この for 文の実行条件 j <= len - i は満たされません. よって,空白は出力されず, * を出力する for 文に処理が進むことになります.

多重ループ

演習4-20 は,難しくはないでしょうが,少し面倒です. 演習4-23 から演習 4-25 は少し難しいです. よく考えてください.

演習問題 4-24 にはヒントがありますね. 高校数学で学習する数列の言葉を使うと, アスタリスク(*)の数は, 初項 1,公差 2 の等差数列です. よって,i 段目の * の数は 1 + (i - 1) * 2 となります. この問題では,i 段目に出力する空白の数も考える必要があります. これにもヒントを出しておきましょう. 最終行(ピラミッドの底辺)から上方向に見ると, 最終行では0個,その上の行では1個,さらにその上の行では2個というように, 必要な空白の数が一つずつ増えることに注目してください.

4-5 プログラムの要素と書式

区切り子の説明(p.103)の最初に, 「キーワードや識別子は,一種の"単語"です」と書かれています. 英文が英単語の集まりから構成されるように, C言語のプログラムも「単語」から構成されます. C言語のプログラムを構成する単語はトークン(token)です. トークンはプログラムを構成する最小単位であり, 識別子,キーワード,定数,文字列リテラル,演算子,区切り子, の6種類があります.

キーワード

このセクションについての補足説明は現在のところありません.

演算子

このセクションについての補足説明は現在のところありません.

識別子

このセクションについての補足説明は現在のところありません.

区切り子

Table 4-6 に示されている 区切り子(punctuator)のほとんどは, すでに使ってきたものです.たとえば,いつも書いてきた


int main(void)
{
  さまざまな文

  return 0;
}

という形式のプログラムでは,( ), { },; という3種類の区切り子が使われています. 最初の int main(void) という部分で, int と void はキーワード, main は識別子(identifier), そして void を囲んでいる ( ) が区切り子です. 続いて,一連の文をまとめて複合文としている記号 { } が区切り子です. 最後に,return 文の後ろにあるセミコロン( ; )が区切り子です. もちろん,return 文だけでなく, 複合文を構成している文それぞれも区切り子 ; で終わっているでしょう.

区切り子 ( ) は,いくつかの異なった用途に使います. すぐ上で述べたように, int main(void) というパターンでの使用は一例です. ここでの ( ) は関数定義での引数を囲んでいます. 関数定義については第6章で学習します. この他に,数学的な計算式での演算順序の指定と, if 文や for 文での制御式に, 区切り子 ( ) を使ってきました. なお,printf 関数など, 関数の呼び出しにおいて引数を囲むのに使われている ( ) は, 区切り子ではなく演算子(operator)に分類されます. 関数呼び出し演算子と呼ばれます. テキスト p.205 の演算子一覧表を参照してください.

まだ学習していないものも含め, 区切り子の用途を以下の表に示します.

区切り子用途
[ ] 配列の添え字(第5章)
( ) 演算順序の指定,制御式を囲む,関数宣言および定義での引数を囲む
{ } 複合文
* ポインタの宣言(第10章)
, 関数宣言および定義での引数を並べる(第6章)
: 名札付きの文(switch 文での case など)
= 変数の初期化
; 文の終了
... 関数宣言および定義での可変個の引数(このテキストでは扱わない)
# プリプロセッサ指令(第5章)

定数と文字列リテラル

このセクションについての補足説明は現在のところありません.

自由形式

このセクションについての補足説明は現在のところありません.

隣接した文字列リテラルの連結

このセクションについての補足説明は現在のところありません.

インデント

インデントについては, 第3章で if 文の入れ子構造(テキスト p.51)を示したときに, すでに説明しました. Emacs でプログラムを書いているときには, 自動的にインデントが行われます. しかし,プログラムの修正によって, インデントが崩れてしまうことがあります. インデントをやり直す方法を,ここでもう一度説明しておきます.

下に示したプログラムはインデントが崩れています. if 文での else の下に,if-else が入れ子になっていることがわかるよう, インデントをやり直します.

Nested if else

コントロール [Ctrl] キーを押しながら, [c] キーを押してください. 続いて,コントロール [Ctrl] キーを押しながら, [q] キーを押してください. これで,下図のように適切なインデントがされます. この操作を C-c C-q と書きます.

Nested if else

まとめ

まとめのプログラム(テキスト p.107)には, 理解することが少し難しい記述があります. 動作をよく考えてみましょう.

(難しすぎるわけではありませんが)最も難しいのは, for 文を使った繰り返しの部分だと思います. for ループを break 文を使って抜け出すための if 文,


if (i * i > x) break;

の意味がわかるでしょうか? プログラムの実行例を見てください. int 型の変数 x の値を 32 とします(この値はキーボードから入力). 面積が 32 となる, 辺の長さが整数値の長方形の,辺の長さの組み合わせが出力されています. ここで,3つの組み合わせ(1 * 32, 2 * 16, 4 * 8)が出力されたあと, 整数値の順序を交換したパターン(すなわち, 32 * 1, 16 * 2, 8 * 4)は出力されていないことに注意してください. カウンタ変数 i の値は 1 からひとつずつインクリメントされていきます. 変数 i の値が 6 になったとき,6 * 6 の値は 36 となって, 変数 x の値である 32 を超えます. これにより上記 if 文の制御式の値が「真」となるので, break 文が実行され,for ループを抜け出します. for 文の制御式(ループの継続条件)は i < x なので, i の値が 32 より小さいときは,ループ本体が実行されてよいはずです. しかし実際には, i の値が 6 になったときにループを抜け出すのです.

この for ループの中で次に書かれている if 文,


if (x % i != 0) continue;

は,次の printf 関数をスキップするためのものです. 出力する辺の長さは整数値でなければならないので, x を i で割ったときに余りが出てはいけません. 余りが出たら,次の printf 関数をスキップし, i の値をインクリメントして, 次のループに入ります.

プログラムの動作を少し追いかけてみましょう. 最初,x が 1 のとき,32 を 1 で割れば余りは出ません. そこで,printf 関数で 1 * 32 と出力します. ここで出力されている 1 は i の値, 32 は x / i の値です. カウンタ変数 i の値を 2 にインクリメントします. 32 を 2 で割れば余りは出ません. そこで,printf 関数で 2 * 16 と出力します. カウンタ変数 i の値を 3 にインクリメントします. 32 を 3 で割れば余り(2)が出ます. そこで,printf 関数はスキップして, i の値を 4 にインクリメントし,次のループに入ります.