Another introduction to programming language C 05: 条件分岐とループ

どんなアルゴリズムも条件分岐とループさえあれば書ける.条件分岐とループはまとめて制御構造と言う.

C言語で一番基本的な条件分岐はif構文である.if構文は

if (A) {
  B;
}
else {
  C;
}

と書く.ブレース(中括弧)を置かないバリエーションもあるが,いいことはひとつもないので,いつもブレースを書くべきである.

if構文では,Aが「真」のときにBが,Aが「偽」のときにCが実行される.else { ... } は省略可能である.Aは条件式と言う.

問題は「真」とはどういうことか,である.本来,真偽はブール代数の数値であって,真や偽にバリエーションは無い.ところが,C言語で一番基本的な単位はint型(整数)である.(C++しか知らない人へ注意.C言語ではcharは基本的な型ではない.)そこで,真偽を表すのにもint型が使われる.整数はいろいろな値を取り得るが,0だけが偽で,0以外はすべて真である.つまり,1であろうが-1であろうが,真である.

それどころではない.C言語ではvoid型関数の呼び出し以外はすべて値を持つから,適当に書いたものは真偽判定に使える可能性が高い.例えば以下のコードは問題無く動く.

if (printf("Hello, world.\n")) {
  printf("Hello, again.\n");
}

関数printfは印字した文字数を返すから,条件式は0以外の値になり,2行目の printf("Hello, again.\n") は実行される.もっとひどい例としてこんなものもある.

if (a = 1) {
  printf("a is 1.\n");
}

さすがにgccは警告を出すが,式 a = 1 は1という値を持つため,「真」と判定される.もしaが1かどうか確かめたければ

if (a == 1) {
  printf("a is 1.\n");
}

とすべきであった.

条件分岐はif構文だけで十分であるが,C言語にはswitch構文というものもある.switch構文は地雷原だ.出来れば避けるべきだが,if構文だけでは入れ子が深くなってしまう場合には使わざるを得ない.使うときは,以下のパタンをまもること!

switch (A) {
  case B:
    B1;
    B2;
    break;
  case C:
    C1;
    C2;
    break;
  default:
    D1;
    D2;
    break;
}

これは A == B のときには B1, B2 を実行し,A == C のときには C1, C2 を実行し,そのどちらでもないときには D1, D2 を実行するという意味である.もしbreakを置き忘れたら,悲惨なことになる.もしbreakが無ければ A == B のときに B1, B2, C1, C2, D1, D2 が実行される.

switch構文のdefault節 D1, D2 が必要なければ,

switch (A) {
  case B:
    B1;
    B2;
    break;
  case C:
    C1;
    C2;
    break;
  default:
    break;
}

のようにしておくことを勧める.これは後で書き換えたくなったときのための,予防接種のようなものだ.

もう一つの制御構造はループである.ループの基本は(C言語では)while構文である.while構文は

while (A) {
  ...
}

で,Aが真である間...が実行される.もちろん,Aがいつも真だと無限ループである.コンパイラは簡単な無限ループさえ簡単に見逃してくれる.

while (1) {
  ...
}

は無限ループであるが,コンパイラは警告しない.ループから脱出する構文はあるが,お勧めしない.

for構文はwhile構文の省略記法である.すなわち,

for (A; B; C) {
  ...
}


A;
while (B) {
  ...
  C;
}

と同じである.for構文では,A, B, C のいずれかあるいは全部を省略出来る.例えば次の書き方はまったく正しい.

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

C言語にはもうひとつdo構文というループ記法があるが,正当な使い道はない.

breakキーワードを使うと,whileだろうとforだろうと(ちょうどswitchのときと同じように)ブロックから抜け出すことが出来る.これがループから抜け出す構文である.ただし,多重ループから抜けることは出来ない.breakが抜け出せるのは一番内側のループだけである.そもそもbreakを使ってループから抜けようという考え方がよくないのである.次の方法は,効率も悪いし,読みやすくもないが,古典的に使われている方法である.

int exit_loop = 0;  /* No */
while (exit_loop == 0) {
  ...
  if (...) {
    exit_loop = 1;  /* Yes */
  }
}

この方法は,何重のループであろうと応用が利く.

ただし,ループを抜け出したい理由が何らかの実行時エラーであった場合は(例えば読み込み中のファイルに不正な値が入っていたとか),エラーメッセージを表示してさっさとexit関数を呼びプログラムを終了させてしまう手もある.近代の言語には例外処理という概念があり,実行時エラーのような例外的状況にはうまくループから抜け出す方法が準備されているのだが,C言語にそのような機能はない.例外的状況でループを抜け出した場合,結局プログラムを終了させることになることがほとんどなので,exit関数でプログラムを終了させることはさほど悪い選択では無い.

C言語にはgotoという構文さえある.gotoはある決まった位置(ラベルを置くためラベル行とも言う)にジャンプするための構文である.gotoは害のないものだが,ラベルは有害である.ラベルを見たときに,どこからジャンプしてきたのか見つけることはほぼ不可能であるからである.goto構文は無かったことにするのが大人のプログラマである.

ループは関数の再帰呼び出しですべて置き換えることが出来る.ただし,C言語は再帰呼び出しをうまく扱えるとは言えないので,格好つけずにループを使うべきである.
Comments