裸の数値だらけのコードは、誰にも読めないコードです。if (state == 2) は何も伝えませんが、if (state == STATE_RUNNING) はすべてを伝えます。enum は、小さく固定された整数の集合にきちんとした名前を与えるC言語の方法です。しかも #define の山とは違って、コンパイラが認識する1つの型としてまとめられます。
enumを定義する
宣言ではタグに名前を付け、波かっこの中に列挙子を並べます。
すぐに気づくべき点が2つあります。1つ目、採番は 0 から始まり1ずつ増えるので、RED は 0、GREEN は 1、BLUE は 2 です。2つ目、列挙子の名前は外側のスコープのどこからでも見えます - Color.GREEN ではなく GREEN と書きます。変数と同じ名前空間に住んでいるため、Cのコードでは慣習的に大文字で叫ぶか、型名を接頭辞に付けて(COLOR_RED)衝突を避けます。
enumの表示には %d を使います。列挙値は整数値そのものだからです。
明示的な値
どの列挙子にも明示的な定数を割り当てられ、その後ろの列挙子はそこから続きます。
値は一意である必要も順序どおりである必要もありません - 2つの列挙子が同じ数値を共有してもよく、これは別名(COLOR_DEFAULT = COLOR_BLACK)にときどき役立ちます。ただしコンパイル時の整数定数でなければならないので、変数から計算することはできません。
よくある定石は、これを使って個数を記録することです。
enum Suit {
CLUBS, DIAMONDS, HEARTS, SPADES,
SUIT_COUNT /* 自動的に 4 - スートを追加しても正しいまま */
};
SUIT_COUNT は本物のスートではなく、その個数であり、自動採番によって手間なく保守されます。ループや配列サイズでこれを使えば、決して古くなりません。
typedef enum
構造体と同じく、C言語での完全な型名は enum Color であり、typedef でキーワードを省けます。
列挙子は 0 から始まる普通の整数なので、そのまま配列の添字にもなります - dx[DIR_EAST] は何の変換もなしに動きます。これがゼロ始まりのenumの、いちばん有用な性質です。
switchの中のenum
enumを switch と組み合わせたとき、この型は本領を発揮します。-Wall を付けてコンパイルすれば、caseを忘れたときにコンパイラが教えてくれます。
enumに STATE_FAILED を追加すると、GCCは enumeration value 'STATE_FAILED' not handled in switch と警告します。この警告はC言語における網羅性チェックにいちばん近いもので、こうしたswitchを default: なしで書くべき強い理由になります - default は警告を黙らせ、新しい状態が未処理のまま通り抜けるのを許してしまいます。代わりに、describe がしているようにフォールバックはswitchの後ろに置きましょう。
enumを文字列に変換する
Cは実行時に名前を保持しません。GREEN は単なる 1 なので、printf("%s", GREEN) は動きようがありません。これを解決するパターンは2つあります。
コンパイラがチェックしてくれる switch 版:
そして ルックアップテーブル 版。短く書けますが、チェックはされません - 配列とenumがずれても、何も警告してくれません。
指示付き初期化子による添字([RED] = "RED")は、少なくとも名前をその枠に結び付けておいてくれるので、enumを並べ替えても文字列が静かに入れ替わることはありません。境界チェックが重要なのは、呼び出し側がenumの範囲外の値を渡すのを何も止められないからです - 次の節を見てください。
enumは鉄壁ではない
enum変数の実体は整数であり、Cは範囲を取り締まりません。
列挙子の算術演算は動き、素のintとの比較も動き、範囲外へのキャストも受け入れられます。ですからenumは、優れたドキュメントであり良いコンパイラへのヒントであって、保証ではないと考えてください。プログラムの外から来る値 - ファイル、引数、ネットワークメッセージ - は、switchにかける前に必ず検証しましょう。
サイズと格納
C17では、すべての列挙子が int に収まらなければなりません。enum Huge { BIG = 3000000000 }; は制約違反で、gcc や clang は既定のモードでは拡張として受け入れるものの、厳格なコンパイラ(-pedantic-errors)は拒否します。列挙子そのものの型は int で、一方enum 型 のサイズは処理系定義です(実際には int)。
(C23ではこの制限が外れ、列挙子が int の範囲を超えてもよくなり、enum E : unsigned long { ... } のように基底の型を指定することさえできます。お使いのツールチェーンがC23を対象にするまでは、列挙子を int の中に収めておきましょう。)
その選択は処理系定義なので、enumを直接バイナリファイルやネットワークパケットに書き込んで、別のマシンで読み戻せると期待してはいけません。境界では <stdint.h> の uint8_t や uint32_t のような固定幅の型に変換しましょう。
enum と #define
どちらも数値に名前を与えます。一般にenumのほうが優れています。
enum | #define | |
|---|---|---|
| 処理するのは | コンパイラ | プリプロセッサ |
| グループ化 | 関連する値が1つの型を共有する | 各定数が独立している |
| 自動採番 | あり | なし、自分で番号を振る |
| デバッガでの可視性 | 多くの場合、名前で見える | 見えない、数値だけ |
switch の網羅性の警告 | あり | なし |
| スコープ | ブロックスコープに従う | テキスト的、#undef まで有効 |
#define に手を伸ばすのは、定数が整数でないとき(文字列、浮動小数点数)か、条件付きコンパイルのようにプリプロセッサ自身が使える必要があるときです。関連する整数の状態、フラグ、種別の集合にはenumを使いましょう。
命名規約
enumは名前が通常のスコープに置かれるため、簡単に衝突します。大規模なCコードベースを正気に保つ規約はこうです。
- 各列挙子に型名を接頭辞として付ける:
COLOR_RED、STATE_IDLE、HTTP_OK。グラフィックスプログラムで接頭辞なしのREDは、いずれ誰か別の人のREDと出会います。 - 列挙子は大文字(SHOUT_CASE)で書き、ほかの定数の見た目に合わせます。
- 型名は単数形のPascalCaseか
snake_case_t:Color、State、color_tを、プロジェクト全体で一貫させます。 - enumでループしたり添字を付けたりするなら、最後に
_COUNTの番兵を追加します。
よくある質問
C言語でenumはどう定義しますか?
enum Color { RED, GREEN, BLUE }; と書くと、3つの名前付き定数を値に持つ型が定義されます。デフォルトでは 0 から採番されるので、RED は 0、GREEN は 1、BLUE は 2 です。変数は enum Color c = GREEN; と宣言します。typedef を添えれば Color c とだけ書けます。
C言語のenumに特定の値を割り当てられますか?
できます:enum Status { OK = 200, NOT_FOUND = 404, ERROR = 500 };。一部だけ指定して残りを続けさせることもできます - 明示的な値を持たない名前は直前の値から続くので、enum { A = 10, B, C }; では B = 11、C = 12 になります。
C言語でenumを文字列に変換するには?
C言語に組み込みの手段はありません。列挙子の名前は実行時には存在しないからです。よく使われるのは、値ごとに文字列リテラルを返す switch を持つ小さな関数か、enumで添字を付ける文字列の配列です。新しい列挙子に case がないと -Wall が警告してくれるぶん、switch版のほうが安全です。
C言語のenumのサイズはいくつですか?
処理系定義ですが、実際には sizeof(int) - ほとんどのシステムで4バイトです。コンパイラはすべての列挙子を格納できる整数型を選びます。バイナリファイルやネットワークパケットのレイアウトを決めるときに特定のサイズを仮定してはいけません。そこでは <stdint.h> の固定幅の型を使いましょう。