C言語の列挙型(enum)は、関連する整数定数に名前を付けてひとまとめに管理できる仕組みです。
可読性と保守性が上がるため、初心者が「定数を乱立させない」第一歩として最適です。
本記事では、enumの基本から書き方、値のルール、実践的な使い方、#define
との違いまで丁寧に解説します。
列挙型(enum)の基本
列挙型とは
列挙型(enum)
は、意味を持つ識別子(列挙子)に整数値を対応づけるユーザー定義型です。
「固定された選択肢を名前で扱える」ことが最大の特徴で、例えば曜日、色、状態、エラーコードのように値の選択肢が限られている場面で使います。
Cでは列挙子は整数定数として扱われ、コンパイラが適切な型サイズを選びます。
列挙型を使うと数値の「意味」がコード上で明確になり、マジックナンバーを排除できます。
数値そのものではなく「名前」で意図を表現できることが、読みやすさに直結します。
使うメリット
列挙型の主なメリットは次の通りです。
まず、定数を1か所に集約できるため変更に強く、設計が整理されます。
またswitch文での網羅性チェックや、デバッガでの表示の分かりやすさなど、開発時の取り回しが向上します。
さらに列挙子の自動連番機能により、共通の桁や順序を簡潔に表現できます。
プリプロセッサの#define
よりも型の情報を保てる点も利点です。
enumの書き方と宣言
基本構文
列挙型はenum
キーワードで宣言します。
列挙子はデフォルトで0から1ずつ増える整数値です。
// 基本的なenum定義と利用例
#include <stdio.h>
// 色を表す列挙型の定義
enum Color {
COLOR_RED, // 0
COLOR_GREEN, // 1
COLOR_BLUE // 2
};
int main(void) {
enum Color c = COLOR_GREEN; // 変数cはenum Color型
printf("c=%d\n", c); // 列挙子は整数として出力できる
return 0;
}
c=1
このように「意味のある名前」で状態を表すと、コードの意図が一目で分かります。
enumの名前と変数の定義
enum 型名 { 列挙子... };
の形で型を宣言し、enum 型名 変数;
で変数を定義します。
型名は必須ではありませんが、後から同じ型を使いたい場合に付けておくと便利です。
#include <stdio.h>
// 型名つきの宣言
enum Status {
STATUS_OK,
STATUS_WARN,
STATUS_ERR
};
int main(void) {
enum Status s1 = STATUS_OK; // 変数宣言
enum Status s2 = STATUS_ERR; // 同じ型を再利用
printf("s1=%d, s2=%d\n", s1, s2);
return 0;
}
s1=0, s2=2
1行で型と変数を同時に定義することもできます。
ただし宣言と利用の場所が離れる場合は、読みやすさのために型と変数の宣言を分けるのがおすすめです。
// 型と変数を同時に宣言する例
enum Direction { DIR_UP, DIR_DOWN, DIR_LEFT, DIR_RIGHT } player_dir, enemy_dir;
補足としてtypedef
で別名を付ける書き方もありますが、詳細は別記事で扱います。
まずは標準的なenum 型名
の形に慣れましょう。
列挙子の命名のコツ
列挙子は大文字スネークケースで、衝突を避けるために接頭辞を付けるのが定番です。
「列挙型名の短い接頭辞」+「意味」で一意にすると、大規模コードでも安全です。
例えばCOLOR_RED
やSTATUS_OK
などです。
Cでは列挙子はスコープに露出するため、短すぎる一般名(OK、ERRORなど)は他の定義と衝突しやすい点に注意します。
列挙子の値と初期値
自動で0から連番
列挙子は値を指定しなければ先頭が0、以降は1ずつ増加します。
これは規格で定められた挙動です。
#include <stdio.h>
enum Weekday { MON, TUE, WED, THU, FRI, SAT, SUN };
int main(void) {
printf("MON=%d TUE=%d WED=%d THU=%d FRI=%d SAT=%d SUN=%d\n",
MON, TUE, WED, THU, FRI, SAT, SUN);
return 0;
}
MON=0 TUE=1 WED=2 THU=3 FRI=4 SAT=5 SUN=6
曜日や月など、自然な並びのある集合にぴったりです。
値を明示するとき
必要に応じて任意の整数値を明示できます。
ビットフラグのようなケースでは意味のある値を割り当てます。
#include <stdio.h>
// アクセス権限をビットで表現
enum Permission {
PERM_READ = 4, // 100b
PERM_WRITE = 2, // 010b
PERM_EXEC = 1 // 001b
};
int main(void) {
printf("R=%d W=%d X=%d\n", PERM_READ, PERM_WRITE, PERM_EXEC);
return 0;
}
R=4 W=2 X=1
「値の意味」が決まっているときは明示値、単純な順序なら自動連番と使い分けると整理しやすくなります。
途中からの連番の続き
一部の列挙子に値を指定すると、その次からは指定した値に続いて自動連番になります。
#include <stdio.h>
enum Error {
ERR_OK = 0,
ERR_WARN = 100,
ERR_WARN_MINOR, // 101
ERR_WARN_MAJOR, // 102
ERR_FAIL = 200,
ERR_FAIL_FATAL // 201
};
int main(void) {
printf("OK=%d WARN=%d MINOR=%d MAJOR=%d FAIL=%d FATAL=%d\n",
ERR_OK, ERR_WARN, ERR_WARN_MINOR, ERR_WARN_MAJOR, ERR_FAIL, ERR_FAIL_FATAL);
return 0;
}
OK=0 WARN=100 MINOR=101 MAJOR=102 FAIL=200 FATAL=201
この性質により区間ごとに意味を持たせる設計がしやすくなります。
同じ値の列挙子に注意
Cでは異なる列挙子に同じ値を割り当ててもエラーにはなりません。
ただし値の重複は判別を難しくし、デバッグ時の混乱のもとです。
#include <stdio.h>
enum Response {
RESP_OK = 200,
RESP_CREATED = 201,
RESP_SUCCESS = 200 // 値が重複
};
int main(void) {
if (RESP_OK == RESP_SUCCESS) {
printf("RESP_OKとRESP_SUCCESSは同じ値です(%d)\n", RESP_OK);
}
return 0;
}
RESP_OKとRESP_SUCCESSは同じ値です(200)
意図せぬ重複を避けるため、接頭辞や体系的な採番を徹底しましょう。
enumの使い方
switch文での分岐
列挙型はswitch
文と相性が良く、分岐の見通しがよくなります。
コンパイラオプションによっては列挙値の漏れを警告させられる場合もあります(-Wswitch
や-Wswitch-enum
など)。
#include <stdio.h>
enum Color { COLOR_RED, COLOR_GREEN, COLOR_BLUE };
const char* color_to_name(enum Color c) {
switch (c) {
case COLOR_RED: return "RED";
case COLOR_GREEN: return "GREEN";
case COLOR_BLUE: return "BLUE";
default: return "UNKNOWN"; // 想定外の値(未初期化や外部入力)に備える
}
}
int main(void) {
enum Color c = COLOR_BLUE;
printf("name=%s\n", color_to_name(c));
return 0;
}
name=BLUE
列挙子を網羅することで、将来の追加や変更にも強い分岐になります。
エラーコードや状態管理
処理結果や状態遷移を列挙型で表現すると、「戻り値の意味」が明確になり、呼び出し側の分岐も読みやすくなります。
#include <stdio.h>
#include <ctype.h>
enum ParseResult {
PARSE_OK,
PARSE_EMPTY,
PARSE_INVALID
};
// 簡単な数値文字列の妥当性チェック
enum ParseResult parse_decimal(const char* s) {
if (s == NULL || *s == '#include <stdio.h>
#include <ctype.h>
enum ParseResult {
PARSE_OK,
PARSE_EMPTY,
PARSE_INVALID
};
// 簡単な数値文字列の妥当性チェック
enum ParseResult parse_decimal(const char* s) {
if (s == NULL || *s == '\0') return PARSE_EMPTY;
for (const char* p = s; *p != '\0'; ++p) {
if (!isdigit((unsigned char)*p)) return PARSE_INVALID;
}
return PARSE_OK;
}
const char* result_to_msg(enum ParseResult r) {
switch (r) {
case PARSE_OK: return "OK";
case PARSE_EMPTY: return "EMPTY";
case PARSE_INVALID: return "INVALID";
default: return "UNKNOWN";
}
}
int main(void) {
const char* samples[] = { "12345", "", "12a3", NULL };
for (int i = 0; i < 4; ++i) {
enum ParseResult r = parse_decimal(samples[i]);
printf("sample[%d] -> %s\n", i, result_to_msg(r));
}
return 0;
}
') return PARSE_EMPTY;
for (const char* p = s; *p != '#include <stdio.h>
#include <ctype.h>
enum ParseResult {
PARSE_OK,
PARSE_EMPTY,
PARSE_INVALID
};
// 簡単な数値文字列の妥当性チェック
enum ParseResult parse_decimal(const char* s) {
if (s == NULL || *s == '\0') return PARSE_EMPTY;
for (const char* p = s; *p != '\0'; ++p) {
if (!isdigit((unsigned char)*p)) return PARSE_INVALID;
}
return PARSE_OK;
}
const char* result_to_msg(enum ParseResult r) {
switch (r) {
case PARSE_OK: return "OK";
case PARSE_EMPTY: return "EMPTY";
case PARSE_INVALID: return "INVALID";
default: return "UNKNOWN";
}
}
int main(void) {
const char* samples[] = { "12345", "", "12a3", NULL };
for (int i = 0; i < 4; ++i) {
enum ParseResult r = parse_decimal(samples[i]);
printf("sample[%d] -> %s\n", i, result_to_msg(r));
}
return 0;
}
'; ++p) {
if (!isdigit((unsigned char)*p)) return PARSE_INVALID;
}
return PARSE_OK;
}
const char* result_to_msg(enum ParseResult r) {
switch (r) {
case PARSE_OK: return "OK";
case PARSE_EMPTY: return "EMPTY";
case PARSE_INVALID: return "INVALID";
default: return "UNKNOWN";
}
}
int main(void) {
const char* samples[] = { "12345", "", "12a3", NULL };
for (int i = 0; i < 4; ++i) {
enum ParseResult r = parse_decimal(samples[i]);
printf("sample[%d] -> %s\n", i, result_to_msg(r));
}
return 0;
}
sample[0] -> OK
sample[1] -> EMPTY
sample[2] -> INVALID
sample[3] -> EMPTY
このように関数の戻り値を列挙型にするだけで、用途が一目瞭然になります。
関数の引数で使う
引数に列挙型を使うと、どの値を渡せばよいかが宣言から読み取れるようになります。
Cでは列挙型は整数型として扱われるため任意の整数も渡せてしまいますが、不正値のチェックを関数内部で行うことで安全性を高められます。
#include <stdio.h>
enum LogLevel { LOG_DEBUG, LOG_INFO, LOG_WARN, LOG_ERROR };
void set_log_level(enum LogLevel level) {
switch (level) {
case LOG_DEBUG:
case LOG_INFO:
case LOG_WARN:
case LOG_ERROR:
printf("Log level set to %d\n", level);
break;
default:
printf("Invalid log level: %d\n", level); // 不正値に備える
break;
}
}
int main(void) {
set_log_level(LOG_INFO);
set_log_level(99); // 列挙型外の値(要チェック)
return 0;
}
Log level set to 1
Invalid log level: 99
APIの意図を型で示し、内部でバリデーションするのがC言語での実践的なパターンです。
#defineの定数との違い
#define
でも定数を作れますが、列挙型は「型情報」と「自動連番」を持つため、メンテナンス性と安全性で有利です。
次の表で違いを比較します。
観点 | enum | #define |
---|---|---|
型情報 | あり(enum 型 )。デバッガで名前が見えることが多い | なし。単なるテキスト置換 |
自動連番 | あり(0から1刻み、途中指定も可能) | なし。手動で定義が必要 |
スコープ | 宣言したスコープ内 | 定義以降どこでも(ヘッダで全域汚染しやすい) |
名称衝突 | 接頭辞で回避しやすい | 衝突しやすい。再定義で不具合に |
コンパイラ支援 | switch 網羅性警告などが期待できる | なし |
デバッグ | 値に対応する名前が追いやすい | 追いにくい(展開後は数値のみ) |
メンテナンス | 集約管理しやすい | バラバラに増えやすい |
迷ったら「選択肢が限定される集合」はenum、算術的に使う一つの定数だけならconst
や#define
を検討、という使い分けが実務的です。
参考として、#define
は意図せず再定義や衝突を招きやすい例を示します。
#include <stdio.h>
// どこかのヘッダで
#define STATUS_OK 0
// 別ファイルでenumにもSTATUS_OKが登場したら…
// enum Response { STATUS_OK = 1, STATUS_ERR = 2 }; // こう書くとマクロが先に展開され混乱のもと
int main(void) {
printf("STATUS_OK=%d\n", STATUS_OK); // ここでは0に展開される
return 0;
}
プリプロセッサはテキスト置換であり、型やスコープの概念がないため、プロジェクトが大きくなるほどenumの利点が際立ちます。
まとめ
列挙型(enum)
は、関連する定数を「意味のある名前」で安全にまとめるための基本機能です。
自動連番や一部の明示値、switch
との相性の良さを活かすことで、マジックナンバーを排除し、意図が読めるコードになります。
さらに、関数の引数や戻り値に使えばAPIの使い方も明快になります。
対して#define
は型がなく衝突もしやすいため、「選択肢の集合」を表すならenumを第一候補にするのが良い指針です。
今日から定数を整理し、読みやすく壊れにくいプログラム作りに役立ててください。