多くの条件に応じて分岐したい時に、if-elseが長く連なると読みづらくなります。
そんな時に活躍するのがswitch文です。
switchは値に応じて分岐を選び、コードの見通しを良くします。
本記事ではcaseとbreakの正しい使い方、defaultの役割、落とし穴とベストプラクティスを、初心者の方にも分かりやすい順序で解説します。
C言語のswitch文の基本
switch文の用途とメリット
多くの離散的な値に応じて処理を切り替える場合、switch文を使うと意図が明確になります。
if-elseでも同じことは可能ですが、switchは分岐対象が単一の式で、候補が列挙できる場面に特に適しています。
読み手はswitch(式)
とcase 定数:
の対応だけを追えば良いので、条件式の詳細を逐一読む必要がありません。
コンパイラ最適化によりジャンプテーブルが使われる場合もあり、実行効率が良くなることがあります。
基本構文
基本形は次の通りです。
各case
の末尾にbreak
を入れるのが標準的な書き方です。
#include <stdio.h>
int main(void) {
int x = 2;
// x の値に応じて分岐
switch (x) {
case 1:
puts("x は 1 です");
break; // ここで switch を抜ける
case 2:
puts("x は 2 です");
break;
default:
// どの case にも一致しない時
puts("x は 1 でも 2 でもありません");
break;
}
return 0;
}
x は 2 です
breakを忘れると、次のcaseに処理が流れ落ちる(フォールスルーする)点がswitch最大の注意点です。
意図しないフォールスルーはバグの温床になります。
対応する型(int, char, enum)
switchの制御式とcaseラベルは、整数系の型で扱います。
浮動小数点や文字列は使えません。
項目 | 許可される型/値 | 補足 |
---|---|---|
switch(式) | 整数型全般(int, short, long, unsigned系, char, enum など) | ポインタや浮動小数点は不可 |
case ラベル | 整数定数式(リテラル、'A' 、enum 定数、#define 定数など) | 実行時に変わる変数は不可 |
charは整数型として扱われます。
'A'
のような文字リテラルは実体は整数です。
一方、"A"
は文字列リテラル(配列)であり、caseでは使えません。
caseとbreakの書き方
caseラベルの書式と定数式
caseラベルは整数定数式である必要があります。
以下のような書き方が可能です。
// 良い例
#define CMD_START 1
enum Level { LOW = 10, MID = 20, HIGH = 30 };
switch (cmd) {
case 0: /* 整数リテラル */
/* ... */
break;
case CMD_START: /* マクロ定数 */
/* ... */
break;
case 'A': /* 文字定数(実体は整数) */
/* ... */
break;
case HIGH: /* enum 定数 */
/* ... */
break;
}
逆に次のような書き方はできません。
変数や計算結果など、実行時に変化する値はcaseに置けないためです。
int a = 10;
switch (x) {
// NG例: 変数や変数を使った式は不可
/* case a: */
/* case a + 1: */
/* case x + 1: */
default:
break;
}
breakの役割と配置
break
は、その位置でswitch文から抜けるためのキーワードです。
breakがないと、次のcaseの文へと処理が流れます(フォールスルー)。
次の例では、case 2でbreakを忘れたため、case 3まで実行されます。
#include <stdio.h>
int main(void) {
int x = 2;
switch (x) {
case 1:
puts("1");
break;
case 2:
puts("2"); // break がない!
case 3:
puts("3"); // ここも実行される
break;
default:
puts("other");
break;
}
return 0;
}
2
3
意図せぬフォールスルーはほぼバグです。
基本は各caseの末尾にbreakを置く習慣をつけてください。
defaultの使い方
default
は、どのcaseにも一致しなかった時に実行される最後の砦です。
defaultの位置はどこでも構いませんが、可読性のため最後に置くのが一般的です。
予期しない値に対しログ出力や防御的なエラー処理を行いましょう。
#include <stdio.h>
int main(void) {
int cmd = 99; // 未知のコマンド
switch (cmd) {
case 1:
puts("START");
break;
case 2:
puts("STOP");
break;
default:
// 予期しない値に備える
fprintf(stderr, "不明なコマンド(%d)です\n", cmd);
break;
}
return 0;
}
不明なコマンド(99)です
複数caseをまとめる書き方
複数の値で全く同じ処理を行う場合、caseを連続して書くと簡潔になります。
#include <stdio.h>
int main(void) {
char c = 'e';
switch (c) {
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
puts("母音です");
break;
default:
puts("子音かその他の文字です");
break;
}
return 0;
}
母音です
fallthroughの意味と注意
fallthrough
(フォールスルー)は、明示的にbreakを書かず、次のcaseの文を続けて実行することです。
意図的に使う場合は、コメントやアトリビュートで明示して誤解や警告を避けます。
#include <stdio.h>
// 接尾辞による倍率: 'G' -> 1024*1024*1024, 'M' -> 1024*1024, 'K' -> 1024
int main(void) {
unsigned long long base = 1;
char suffix = 'M';
switch (suffix) {
case 'G':
base *= 1024;
/* FALLTHROUGH */ // 意図的に次へ
case 'M':
base *= 1024;
/* FALLTHROUGH */ // 意図的に次へ
case 'K':
base *= 1024;
break;
default:
// 倍率なし
break;
}
printf("%llu\n", base);
return 0;
}
1048576
コンパイラによっては/* FALLTHROUGH /
や/ fall through */
というコメントで意図を示すと警告を抑制できます。
新しめの規格やコンパイラでは[[fallthrough]];
の属性が使える場合があります(対応状況はコンパイラのドキュメントを確認してください)。
よくあるエラーと注意点
break書き忘れによる誤動作
breakを忘れると、想定外のcaseまで実行されます。
例えばメニュー処理で「1:開始、2:停止」としていて、2の処理の後にbreakがないと、停止の直後に開始が実行される、といった事故が起こります。
各case末尾のbreakを習慣化し、意図的なフォールスルーには必ずコメントを添えましょう。
case値の重複エラー
同じ値のcaseラベルはひとつのswitch内に重複できません。
コンパイルエラーになります。
// コンパイルエラーの例
switch (42) {
case 1:
break;
case 1: // 重複
break;
default:
break;
}
コンパイルエラー(例):
error: duplicate case value '1'
note: previously used here
文字リテラル(‘A’)と数値の違い
文字リテラル'A'
は整数(多くの処理系で65)で、caseに使えます。
一方"A"
は文字列リテラルで、caseでは使えません。
さらにCでは文字リテラルの型はintである点も覚えておくと良いです。
#include <stdio.h>
int main(void) {
printf("'A' の整数値: %d\n", 'A');
printf("65 と 'A' は等しいか: %s\n", (65 == 'A') ? "true" : "false");
switch ('A') {
case 'A': // OK
puts("マッチします");
break;
/* case "A": */ // NG: 文字列リテラルは不可
default:
break;
}
return 0;
}
'A' の整数値: 65
65 と 'A' は等しいか: true
マッチします
case直後の変数宣言はブロックで囲む
label(ここではcase)の直後には文(statement)が必要です。
変数宣言は文ではないため、複合文(ブロック)で囲んでください。
// NG例
/*
switch (n) {
case 1:
int a = 0; // エラーになりやすい
a++;
break;
}
*/
// OK例: ブロックで囲む
switch (n) {
case 1: {
int a = 0;
a++;
break;
}
default:
break;
}
範囲指定ができない時の対処
標準Cのswitchはcaseで範囲(1〜5など)を直接指定できません。
次のような回避策が実用的です。
- 値を前処理して类别化する。例: 点数
score
を10で割って十の位で分岐する。 - 列挙できる範囲ならcaseを並べてまとめる。
- 範囲など複雑な条件は素直にif-elseにする。
#include <stdio.h>
int main(void) {
int score = 86;
// 十の位で段階化してから switch
switch (score / 10) {
case 10: // 100点
case 9:
puts("評価: A");
break;
case 8:
puts("評価: B");
break;
case 7:
puts("評価: C");
break;
case 6:
puts("評価: D");
break;
default:
puts("評価: F");
break;
}
return 0;
}
評価: B
GCCなどにはcase 1 ... 5:
のような拡張がありますが、標準Cではありません。
可搬性を重視するコードでは避けましょう。
switch内のcontinueの挙動
continueはループを次の反復に進めるキーワードです。
switch単体の中で使っても意味はなく、そもそもループ外ではコンパイルエラーになります。
ループ内のswitchでcontinue
を書くと、switchを抜けるのではなくループの次回反復へ進みます。
#include <stdio.h>
int main(void) {
for (int i = 0; i < 5; i++) {
switch (i) {
case 2:
puts("i=2: continue で次の反復へ");
continue; // ループ( for )の次反復へ。下の puts は実行されない
case 3:
puts("i=3: break は switch を抜けるだけ");
break; // for には留まり、下の puts は実行される
default:
printf("i=%d: 通常処理\n", i);
break;
}
puts("after switch"); // continue された反復では到達しない
}
return 0;
}
i=0: 通常処理
after switch
i=1: 通常処理
after switch
i=2: continue で次の反復へ
i=3: break は switch を抜けるだけ
after switch
i=4: 通常処理
after switch
switchを抜けたいならbreak、ループを抜けたいならループ側のbreakやフラグ管理などを使い分けましょう。
実用例とベストプラクティス
メニュー選択のswitch-case
コンソールメニューはswitchの定番です。
defaultで不正入力を検出し、各case末尾にbreakを忘れないのが基本です。
#include <stdio.h>
int main(void) {
int choice = 0;
int a = 12, b = 3;
puts("メニュー:");
puts(" 1) 加算");
puts(" 2) 減算");
puts(" 0) 終了");
printf("選択を入力してください: ");
if (scanf("%d", &choice) != 1) {
fprintf(stderr, "入力エラーです\n");
return 1;
}
switch (choice) {
case 1:
printf("%d + %d = %d\n", a, b, a + b);
break;
case 2:
printf("%d - %d = %d\n", a, b, a - b);
break;
case 0:
puts("終了します");
break;
default:
fprintf(stderr, "不正な選択(%d)です\n", choice);
break;
}
return 0;
}
メニュー:
1) 加算
2) 減算
0) 終了
選択を入力してください: 1
12 + 3 = 15
enumを使った安全な分岐
enumは有効な値の集合を型として表現でき、switchと相性が良いです。
未知の値はdefaultで検知します。
#include <stdio.h>
typedef enum { ACT_EXIT = 0, ACT_ADD = 1, ACT_SUB = 2, ACT_MAX } Action;
void do_action(Action act, int x, int y) {
switch (act) {
case ACT_ADD:
printf("ADD: %d\n", x + y);
break;
case ACT_SUB:
printf("SUB: %d\n", x - y);
break;
case ACT_EXIT:
puts("EXIT");
break;
default:
// 範囲外の値を検知
fprintf(stderr, "不正な Action(%d) です\n", act);
break;
}
}
int main(void) {
do_action(ACT_ADD, 7, 5);
do_action(ACT_SUB, 10, 3);
// do_action(99, 1, 1); // 故意に範囲外(環境次第でコンパイルエラー)
return 0;
}
ADD: 12
SUB: 7
マクロ定数とswitchの組み合わせ
外部仕様やプロトコルの定数を#define
で表し、switchで分岐するのは一般的です。
ただし型安全性はenumの方が高いため、可能ならenumの採用を検討しましょう。
#include <stdio.h>
#define MSG_HELLO 100
#define MSG_GOODBYE 200
void handle_message(int msg) {
switch (msg) {
case MSG_HELLO:
puts("Hello!");
break;
case MSG_GOODBYE:
puts("Goodbye!");
break;
default:
fprintf(stderr, "未知のメッセージ(%d)\n", msg);
break;
}
}
int main(void) {
handle_message(MSG_HELLO);
handle_message(123); // 未知
return 0;
}
Hello!
未知のメッセージ(123)
defaultでエラー処理を徹底
defaultは単なる「その他」ではなく、不整合検知の最後の砦です。
ログ出力、例外終了(組み込み環境でなければabort()
等)、安全なデフォルト動作を入れるなど、プロジェクトの方針に従って徹底します。
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int state = 3; // 想定: 0,1,2 のみ
switch (state) {
case 0: puts("Idle"); break;
case 1: puts("Run"); break;
case 2: puts("Sleep"); break;
default:
fprintf(stderr, "致命的: 未知の状態(%d)\n", state);
/* abort(); */ // 必要なら強制終了
break;
}
return 0;
}
致命的: 未知の状態(3)
コメントで意図的fallthroughを明示
フォールスルーが設計意図であることをコメントで伝えると、レビューや将来の修正での事故を防げます。
コンパイラ警告抑制にも有効です。
#include <stdio.h>
void log_with_level(int level, const char *msg) {
// level: 0=ERROR, 1=WARN, 2=INFO
switch (level) {
case 0:
puts("[ERROR]");
/* FALLTHROUGH */ // ERROR は WARN/INFO 相当も含む
case 1:
puts("[WARN]");
/* FALLTHROUGH */
case 2:
puts("[INFO]");
puts(msg);
break;
default:
fprintf(stderr, "未知のレベル(%d)\n", level);
break;
}
}
int main(void) {
log_with_level(0, "重大な問題");
return 0;
}
[ERROR]
[WARN]
[INFO]
重大な問題
対応しているコンパイラ/規格では[[fallthrough]];
の属性を使用することも可能です。
いずれにせよ、意図を明文化することが重要です。
まとめ
switch文は単一の式の離散値で分岐する際に、if-elseよりも読みやすく安全に書ける強力な構文です。
caseラベルには整数定数式を用い、各case末尾のbreak
を徹底し、default
で予期しない値を確実に扱いましょう。
フォールスルーは意図がある場合のみに限定し、コメントや属性で明示します。
範囲条件が必要な時は前処理(例: 除算で類別)やif-elseに切り替えると、正しさと可読性を両立できます。
enumの活用やログ/エラー処理の徹底と合わせて、安全で意図が伝わる分岐コードを書いていきましょう。