1つの値、たくさんの定数
同じ変数を定数と比べ続ける else if の連鎖は、C言語に専用の文がある定番のパターンです。
if (choice == 1) { /* ... */ }
else if (choice == 2) { /* ... */ }
else if (choice == 3) { /* ... */ }
else { /* ... */ }
switch はそれをもっと直接的に表現します。式を一度だけ評価し、一致するラベルへ直接ジャンプします。
choice を変えてもう一度実行してみてください。この構造は、許される値の集合を一目で見えるようにします。ここで switch を選ぶ本当の理由はそれです。
構文とcaseラベルの規則
switch (integerExpression) {
case CONSTANT_1:
statements
break;
case CONSTANT_2:
statements
break;
default:
statements
break;
}
C言語が強制する4つの制約があります。
- switchの式は整数型でなければなりません -
int、char、short、long、あるいはenumです。floatもdoubleも文字列も不可です。 - どの
caseラベルも、コンパイル時に分かる定数式でなければなりません。nが変数のときのcase n:はエラーで、case 3 + 4:やcase MAX:(#defineかenum定数)は問題ありません。 - 2つのcaseが同じ値を持つことはできません。重複はコンパイルエラーで、小さいながら本物の安全網になります。
- caseは範囲を表せません。
case 1 ... 5:はGCCの拡張であって標準Cではありません。範囲にはifを使いましょう。
default は省略可能で、ブロックのどこに置いても構いませんが、最後に置くのがほぼ普遍的な慣習です。default がなければ、一致しない値は単にswitch全体を飛ばします。
break:省略できない理由
C言語の switch で最も意外なのは、case ラベルが箱ではなくジャンプ先だということです。いったんラベルに着地すると、break か閉じ波かっこに出会うまで、その下のすべての文を - ほかのcaseラベルを跨いで - 実行し続けます。
最初のswitchは 1、2、3 をすべて表示します - case 1 に一致し、その後のすべてを実行したからです。2つ目は 1 だけを表示します。この挙動がフォールスルーと呼ばれるもので、break の書き忘れは古典的なC言語のswitchバグです。コンパイルに失敗するのではなく、プログラムが黙って間違うからです。
gcc -Wall -Wextra -Wimplicit-fallthrough でコンパイルすれば、次のcaseへ流れ込んでいる箇所をコンパイラがすべて指摘してくれます。
最後のcaseの break は厳密には冗長です - どのみちそこでブロックが終わるからです。それでも書いておきましょう。後でその下に新しいcaseを足したときに、静かにフォールスルーが生まれないようにするためです。
意図的なフォールスルー
フォールスルーは、意図して使うなら機能です。間に文を挟まずにラベルを積み重ねるのが、「これらの値は同じことをする」と言うふつうのやり方です。
3つのラベルが1つの本体を共有し、コードの重複も長い || の条件もありません。この形についてコンパイラは警告しません。フォールスルーする文がそもそも存在しないからです。
間に文を挟むフォールスルー - case 1 が実行され、それから意図的にcase 2 へ続く形 - はもっと珍しく、明示的なコメントに値します。break がないのが決断だったことを、読み手に伝える必要があるからです。
switch (level) {
case 3:
printf("verbose\n");
/* fall through */
case 2:
printf("info\n");
/* fall through */
case 1:
printf("errors\n");
break;
}
/* fall through */ はGCCの警告が認識するコメントなので、診断も同時に黙らせてくれます。
文字でのswitch
char は整数型なので、文字の分類は自然にはまります。
case 'a': がシングルクォートを使っていることに注目してください - これは文字定数で、その文字の整数値そのものです。case "a": は文字列になるのでコンパイルできません。
enumでのswitch
C言語で最良の組み合わせは、switch と enum です。caseラベルが読める名前になり、すべての列挙子を扱っていないときにコンパイラが警告できます。
ここには真似する価値のあることが2つあります。各caseが return しているので break は不要です - return は関数そのものを抜けるので、switchも同時に終わります。そして default がありません。-Wswitch(-Wall に含まれます)を使うと、default を省いたことで、誰かが5つ目の状態を追加してこの関数を更新し忘れた日にコンパイラが警告してくれます。default があれば、それを覆い隠していたでしょう。
switchの中で変数を宣言する
switch の本体は1つのブロックなので、あるcaseでの宣言はその後のcaseからも見えます - 初期化されているとは限りませんが。C言語では初期化を飛び越えるのはエラーです。
switch (n) {
case 1:
int x = 10; // C言語ではエラー:ラベルがこの形で宣言に先行できない
printf("%d\n", x);
break;
case 2:
printf("%d\n", x); // x はここにも存在するが、一度も初期化されていない
break;
}
caseがローカル変数を必要とするときは、そのcaseを自前の波かっこで包みましょう。
switch (n) {
case 1: {
int x = 10;
printf("%d\n", x);
break;
}
case 2:
printf("two\n");
break;
}
switchか、if-elseか
次のすべてが成り立つときは switch に手を伸ばしましょう。テストするのが1つの式であること、それが整数型であること、そして固定された定数と突き合わせていること。メニューの処理、enumの状態機械、文字の分類、コマンドのディスパッチが代表例です。得られるのは読みやすさ、重複値のチェック、そして未処理の列挙子について警告できるコンパイラです。
テストが範囲(score >= 90)、浮動小数点数、文字列(strcmp が要る)、複数の別々の変数、あるいは && や || で組み合わせた条件を含むときは if-else に手を伸ばしましょう。どれもcaseラベルでは表せません。
性能が決め手になることはまれです。コンパイラは密なswitchをジャンプテーブルに、疎なswitchを比較の列に変えるので、ケースが数個なら両者は見分けがつきません。意図を言い表しているほうを選びましょう。
よくある質問
C言語のswitch文はどう動きますか?
switch (expr) は整数式を一度だけ評価し、その値に一致する定数を持つ case ラベルへジャンプして、break かswitchの終わりに達するまでそこから実行を続けます。どれにも一致しなければ、default があればそこへジャンプし、なければブロック全体を飛ばします。
C言語のswitchでなぜbreakが必要なのですか?
case ラベルは別々のブロックではなく、単なるジャンプ先だからです。break がないと、実行はそのまま次のcaseの文へ流れ込みます - これをフォールスルーと呼びます。break の書き忘れはC言語のswitchで最もよくあるバグで、gcc -Wimplicit-fallthrough が指摘してくれます。
C言語で文字列をswitchできますか?
できません。C言語の switch は整数式を要求するので、int、char、enum の値は使えますが、文字列・浮動小数点数・範囲は使えません。文字列で分岐するには、if-elseの連鎖の中で strcmp を使うか、まず文字列をenumに対応づけてそれをswitchしましょう。
C言語でif-elseではなくswitchを使うべきなのはどんなときですか?
1つの整数や文字を固定された定数のリストと突き合わせるとき - メニューのコマンド、enumの状態、文字の分類 - に switch を使います。テストが範囲や複数の変数を含むとき、あるいはコンパイル時定数との等価比較以外のときは if-elseの連鎖 を使いましょう。