C言語 enum 列挙型【簡単!重複しない連番を作り出す方法】

C言語
この記事は約17分で読めます。

こんにちは、ナナです。

構造体に続いて、ユーザー定義型の1つである、enum(列挙型)を紹介しましょう。

例えばスーパーに行くとたくさんの商品が置いてあります。実はそれらの商品には異なる数値が紐づけられており値段などがわかる仕組みになっているのです。

この商品に付ける数値で大事なことは「重複しない数であること」です。このような連番が欲しくなるシーンで活躍するのが列挙型(enum)です。

列挙型は構造体と似た部分があるため、構造体を知らない方は『C言語 構造体 struct【情報のパッケージ化とそのメリット】』を事前に読んでおくとよいです。

本記事では次の疑問点を解消する内容となっています。

本記事で学習できること
  • 列挙型のenumって何だろう?何をしてくれるの?
  • enumの定義方法ってどうやるの?
  • enumでtypedefを使ったときの定義方法とは?
  • enumを使うメリットって何があるの?
  • enumを使う時の注意点とは?

では、enumの使い方を学んでいきましょう。

スポンサーリンク

重複しない番号が必要なシーンを学ぶ

見てください!あの幻の人気クレープ店「オ・イシイ・クレ・プ」の整理券をなんと手に入れたんです。0x0141C番、楽しみです。並ばなくても夕方くらいに行けば食べられルンルン♬

整理券の発行
ナナ
ナナ

整理券が16進数だね、10進数で5148番だよ。絶望的な数字だね。本当にあってるのかな?(怪しい人影が見えるけど…)
それじゃあ、番号にまつわるC言語の機能を今日は紹介しようね。連続した番号を発行する「列挙型」と呼ばれる仕組みがあるよ。

管理すべき情報に番号を付けたい、このようなことって結構あるんです。列挙型はこのような番号を作り出す仕組みです。

重複しない番号を管理したいシーン

switch文を扱った記事で、次のようにフルーツの種類にID番号を関連付けた問題がありました。

#include <stdio.h>

int main(void)
{
    //  フルーツの番号を指定
    int fruit = 0;

    switch (fruit)
    {
    case 0:
        printf("オレンジは150円です");
        break;

    case 1:
        printf("バナナは80円です");
        break;

    case 2:
        printf("桃は400円です");
        break;
    }

    return 0;
}

このように、あるものに対して重複しない一意の番号を関連付けたいシーンは、開発の中でよく出てきます。こんな時に便利なのがenumの列挙型です。

スポンサーリンク

連番の作り方:enum 列挙型の定義と使い方

列挙型は番号を作る仕組みなんですね。私、筋トレするときに各筋肉に番号を付けて順番に鍛えてるんです。これも、列挙型にできるのかな?

列挙型の作り方を教えてください。

ナナ
ナナ

よし、じゃあ列挙型の定義方法を教えるよ。構造体とは似てる部分と違う部分があるから、そこに気を付けながら読んでね。

列挙型は連番の定数を生成するための仕組みです。enumイーナム・エニュームというキーワードで定義します。

定数とは「定められた数」であり、プログラム実行中に値が変わらない数値のことを言います。変数は「変わる数」なのでプログラム実行中に数値を変更できますよね。

列挙型の定数定義の方法

列挙型は次のように定義します。

enum
{
    E_FRUIT_ORANGE,      //  0:オレンジ
    E_FRUIT_BANANA,      //  1:バナナ
    E_FRUIT_PEACH,       //  2:桃
};

定数の項目である「列挙子」を順に並べて定義します。構造体と異なりカンマ区切りで指定することに注意しましょう。

列挙型の特徴は、列挙子を並べた順番に番号が自動採番されることです。

フルーツの番号

デフォルトで最初の列挙子には「0」の定数が与えられますが、次のように「リンゴ」に直接「10」という数値を紐づけることもできます。

enum
{
    E_FRUIT_ORANGE,      //  0:オレンジ(最初は0番)
    E_FRUIT_BANANA,      //  1:バナナ
    E_FRUIT_PEACH,       //  2:桃
    E_FRUIT_APPLE = 10,  // 10:リンゴ
    E_FRUIT_GRAPE ,      // 11:ぶどう(ひとつ前+1)
};

数値を指定しない場合は、ひとつ前の列挙子の値に+1したものが自動で割り振られます。そのため、ぶどうの値は「11」となります。

列挙型の変数の初期化と代入

列挙型はint型と等しいため、int型の変数に直接、初期化や代入ができます。

//int型のfruit変数に0を入れるのと同じ役割
int fruit = E_FRUIT_ORANGE;
スポンサーリンク

enum 列挙型定義で使用するtypedef

騙されました…。あのクレープ屋さんに整理券のシステムなんてなかったんです。クレープ食べられませんでした。えーん(泣)。
あの整理券売ってた人、次見つけたらこの筋肉で必ず締め上げます。

ナナ
ナナ

うーん、やっぱりね。16進数の整理券とか見たことないからね。犯人はソフト開発に詳しい人だよ。でも、僕じゃないから締め上げないでね。

気を取り直して列挙型を学ぼう。typedefを列挙型でも使用することがあるんだ。

構造体と同様にtypedefして列挙型の型定義ができます。

// 列挙型に型名を付けて定義
typedef enum
{
    E_FRUIT_ORANGE = 0,  //  0:オレンジ
    E_FRUIT_BANANA,      //  1:バナナ
    E_FRUIT_PEACH,       //  2:桃
} E_FRUIT;

// 列挙型名 + 変数名 で変数定義
E_FRUIT fruit = E_FRUIT_ORANGE;

構造体と同じように、typedefを使って列挙型に型名(E_FRUIT)を与えることもできます。このときの変数定義は「列挙型名 変数名;」となりますね。

列挙型に名前をつけた場合は、変数に設定すべき列挙子が、型名で関連付くためわかりやすくなるメリットがあります。

例えば、フルーツ以外にも列挙型の定数が登場すると、定数が乱立するため型名を決めておいた方が管理がしやすくなります。

// フルーツの列挙子一覧
typedef enum
{
    E_FRUIT_ORANGE,      //  0:オレンジ
    E_FRUIT_BANANA,      //  1:バナナ
    E_FRUIT_PEACH,       //  2:桃
}E_FRUIT;

// 野菜の列挙子一覧
typedef enum
{
    E_VEGETABLE_PUMPKIN, //  0:かぼちゃ
    E_VEGETABLE_CABBAGE, //  1:キャベツ
    E_VEGETABLE_POTATO,  //  2:ジャガイモ
}E_VEGETABLE;

//  商品番号(型名は野菜の列挙型名)
E_VEGETABLE itemID;

//	itemIDのデータ型はE_VEGETABLE
itemID = E_FRUIT_ORANGE;		//	不正解(フルーツ)
itemID = E_VEGETABLE_PUMPKIN;	//	正解(野菜)
ナナ
ナナ

列挙型の正体はただの数値です。そのため、データ型の異なる列挙子を代入しても警告もエラーも出ないことに注意が必要です。

スポンサーリンク

enum 列挙型を使うことのメリット

列挙型の定義方法はわかりましたが、いったい何が便利なんですか?いまいち嬉しさがしっくり来ていません。この筋肉ですら列挙型に戸惑っています。

ナナ
ナナ

では、列挙型を使うことでのメリットをプログラムを使って解説するよ。

列挙型のメリット① 数値に名前が付くことでの可読性の向上

列挙子は数値の代わりとして、記述することができます。

列挙子は、開発者の目には文字として見えますが、コンピュータには数値として見えています。

これにより無味乾燥な数値というものに、文字として意味が紐づくため、私たち開発者にとってはプログラムの可読性が向上します。これが最初の列挙型のメリットです。

typedef enum
{
    E_FRUIT_ORANGE,      //  0:オレンジ
    E_FRUIT_BANANA,      //  1:バナナ
    E_FRUIT_PEACH,       //  2:桃
}E_FRUIT;

//  0はオレンジだけど意味が読み取れない
int     fruit1 = 0;

//  オレンジを設定しているのが明確
E_FRUIT fruit2 = E_FRUIT_ORANGE;
ナナ
ナナ

「数値に意味を付ける」これが列挙型の代表的なメリットです。このありがたさは、システム規模が大きくなるほど感じられるようになります。

列挙型のメリット② 定数の自動採番機能

列挙型の良さは定数値を自動で割り付けてくれることです。そのため、重複した番号を間違って採番してしまうといったヒューマンエラーを取り除くことができます。

例えば、リンゴを新しくフルーツとして登録したい場合、最後に追加すると桃の番号+1である3番が自動で割りつきます。

typedef enum
{
    E_FRUIT_ORANGE,      //  オレンジ(0)
    E_FRUIT_BANANA,      //  バナナ(1)
    E_FRUIT_PEACH,       //  桃(2)
    E_FRUIT_APPLE,       //  リンゴ(3)
}E_FRUIT;

では、バナナの次にリンゴを追加した場合はどうなるでしょう。この場合、リンゴは2番が採番され、桃は3番として採番され直します。

typedef enum
{
    E_FRUIT_ORANGE,      //  オレンジ(0)
    E_FRUIT_BANANA,      //  バナナ(1)
    E_FRUIT_APPLE,       //  リンゴ(2)
    E_FRUIT_PEACH,       //  桃(3)
}E_FRUIT;
ナナ
ナナ

このように、列挙子の並びによって自動的に採番し直してくれるのが列挙型の特徴です。手作業で番号を振らなくてよいので採番間違えが起きないです。

列挙型のメリット③ switch文との相性が抜群によい

列挙型は数値の羅列です。

そのため、数値の一致判定にて条件分岐するswitch文との相性が抜群によいです。case文に列挙子を記述するテクニックは幅広く使われています。

#include <stdio.h>

typedef enum
{
    E_FRUIT_ORANGE,      //  0:オレンジ
    E_FRUIT_BANANA,      //  1:バナナ
    E_FRUIT_PEACH,       //  2:桃
}E_FRUIT;

int main(void)
{
    E_FRUIT fruit = E_FRUIT_ORANGE;

    // 列挙子をcase文で並べて一致判定を行う
    switch (fruit)
    {
    case E_FRUIT_ORANGE:
        printf("みかん");
        break;

    case E_FRUIT_BANANA:
        printf("バナナ");
        break;

    case E_FRUIT_PEACH:
        printf("桃");
        break;

    default:
        printf("不明");
        break;
    }

    return 0;
}
ナナ
ナナ

switch文は値によって進む道を分岐するので、列挙型のようなID管理を得意とする機能と組み合わせると力を発揮しますね。

スポンサーリンク

enum 列挙型を使う時の注意点

筋トレした後はしっかりと筋肉を休ませないといけないんです。列挙型を使う時に注意した方がよいことってありますか?

ナナ
ナナ

では、列挙型を扱う時に知っておいた方がよい注意点を説明するよ。

列挙型の変数には列挙子以外の数値を入れても警告もエラーも出ない

列挙子という定数の枠組みを作り出しても、変数への設定に関して特別な制約は発生しません。

#include <stdio.h>

// フルーツの列挙子一覧
typedef enum
{
    E_FRUIT_ORANGE,      //  0:オレンジ
    E_FRUIT_BANANA,      //  1:バナナ
    E_FRUIT_PEACH,       //  2:桃
}E_FRUIT;

int main(void)
{
    E_FRUIT fruit;

    //  列挙子にない数値を入れても警告されない
    fruit = 100;

    return 0;
}

このように列挙子に存在しない「100」を代入しても、特に警告もエラーも発生しません。

列挙子には重複した値を禁止しているわけではない

列挙子の特徴は、自動採番機能により重複する番号を作らないことですが、皆さんが値を指定することで重複させることも可能です。

#include <stdio.h>

// フルーツの列挙子一覧
typedef enum
{
    E_FRUIT_ORANGE,       //  0:オレンジ
    E_FRUIT_BANANA = 5,   //  5:バナナ
    E_FRUIT_PEACH  = 5,   //  5:桃
}E_FRUIT;

int main(void)
{
    E_FRUIT fruit;

    //  バナナを代入
    fruit = E_FRUIT_BANANA;
    printf("%d\n", fruit);

    //  桃を代入
    fruit = E_FRUIT_PEACH;
    printf("%d\n", fruit);

    return 0;
}

バナナと桃に同じ「5」の値を設定しました。動作結果は次のように共に「5」の数値が表示されています。

5
5

つまり、列挙型は値を省略したときに、一つ前の項目値に+1するというルールしかなく、重複を禁止している機能ではありません。

スポンサーリンク

Q&A:enum 列挙型でよくある質問

ナナ
ナナ

enumの列挙型について質問していいよ。

Q:列挙型の型名や列挙子の名前が「E_FRUIT~」のようになっているけどルールがあるの?

列挙型の定数定義方法をマスターしたんです。見てください!
この定義を使って筋力トレーニングの組み立てをやるんです。この筋肉を鍛えあげて必ずあいつを…。

// 筋肉の列挙子一覧
typedef enum
{
    E_MUSCLE_PECTORALIS_MAJOR = 0, //  大胸筋
    E_MUSCLE_DELTOID,              //  三角筋
    E_MUSCLE_BICEPS_BRACHII,       //  上腕二頭筋
    E_MUSCLE_LATISSIMUS_DORSI,     //  広背筋
    E_MUSCLE_RECTUS_ABDOMINIS,     //  腹直筋
}E_MUSCLE;

このように例にならって列挙型を定義したのですが、E_MUSCLEの「E_」ってなんですか?それと全部名前が大文字になっているんですけど、大文字でないと定義できないのでしょうか?

ナナ
ナナ

うん、筋肉の種類が列挙型で定義されたのは、おそらく世界で初めてだと思うよ。定義方法は完ぺきマスターしてるね。筋肉に対する情熱と、何かに対する怨念が感じられるね。

名前の付け方は自由なんだけど、慣例があるから知っておくといいね。

「E_」とは、enumの「E」を表現しており、一目で「列挙型だね」とわかる接頭語を付けています。

実際の開発では、このように名前に一定のルールを設けて運用することは、珍しくありません。開発に関わる全ての人がプログラムを理解しやすくする効果があります。

これを「コーディングルール」と呼びます。多くの企業では独自のコーディングルールを内部に持っています。名前だけでなく様々なプログラムを作るルールを定めて運用しているのです。

コーディングルール

また、列挙型のような定数定義は、C言語では全て大文字で定義するという慣例があります。

ですので、皆さんも他の人のプログラムを見て大文字の単語を見つけたら、「これはたぶん定数だな」という視点で読むとよいでしょう。

Q:列挙型の範囲チェックはどうやるのがいいの?

関数の引数に列挙型の変数を指定したいんです。この場合、列挙型変数の値がちゃんとした値なのか範囲チェックをしたくて、こんな感じでいいですか?

// 筋肉の列挙子一覧
typedef enum
{
    E_MUSCLE_PECTORALIS_MAJOR = 0, //  大胸筋
    E_MUSCLE_DELTOID,              //  三角筋
    E_MUSCLE_BICEPS_BRACHII,       //  上腕二頭筋
    E_MUSCLE_LATISSIMUS_DORSI,     //  広背筋
    E_MUSCLE_RECTUS_ABDOMINIS,     //  腹直筋
}E_MUSCLE;

int printMuscle(E_MUSCLE muscle)
{
    //  筋肉種別の範囲チェック
    if (muscle < E_MUSCLE_PECTORALIS_MAJOR || E_MUSCLE_RECTUS_ABDOMINIS < muscle)
    {
        //  そんな筋肉はないため異常
        return -1;
    }

    …(処理)…
}
ナナ
ナナ

このチェック方法は現時点ではいいけど、将来性という意味では少し課題があるね。それは、筋肉の種類を増やそうとした時にチェック範囲を変更する必要があるからだね。

次のように、新しい筋肉である「大腿四頭筋だいたいしとうきん」を追加した際に、列挙型の有効範囲が変わってしまいますね。

定義の追加によって、範囲チェックの処理も変更する必要があるため保守性が悪いのです。

// 筋肉の列挙子一覧
typedef enum
{
    E_MUSCLE_PECTORALIS_MAJOR = 0, //  大胸筋
    E_MUSCLE_DELTOID,              //  三角筋
    E_MUSCLE_BICEPS_BRACHII,       //  上腕二頭筋
    E_MUSCLE_LATISSIMUS_DORSI,     //  広背筋
    E_MUSCLE_RECTUS_ABDOMINIS,     //  腹直筋
    E_MUSCLE_BICEPS_FEMORIS,       //  大腿四頭筋(追加)
}E_MUSCLE;

int printMuscle(E_MUSCLE muscle)
{
    //  筋肉種別の範囲チェック
//  if (muscle < E_MUSCLE_PECTORALIS_MAJOR || E_MUSCLE_RECTUS_ABDOMINIS < muscle)
    if (muscle < E_MUSCLE_PECTORALIS_MAJOR || E_MUSCLE_BICEPS_FEMORIS < muscle)
    {
        //  そんな筋肉はないため異常
        return -1;
    }

    …(処理)…
}

こんな場面では列挙型の最後に「番兵」を配置するテクニックがあります。

// 筋肉の列挙子一覧
typedef enum
{
    E_MUSCLE_PECTORALIS_MAJOR = 0, //  大胸筋
    E_MUSCLE_DELTOID,              //  三角筋
    E_MUSCLE_BICEPS_BRACHII,       //  上腕二頭筋
    E_MUSCLE_LATISSIMUS_DORSI,     //  広背筋
    E_MUSCLE_RECTUS_ABDOMINIS,     //  腹直筋
    E_MUSCLE_BICEPS_FEMORIS,       //  大腿四頭筋

    //---
    E_MUSCLE_END                   //  番兵
}E_MUSCLE;

int printMuscle(E_MUSCLE muscle)
{
    //  筋肉種別の範囲チェック
    if (muscle < 0 || E_MUSCLE_END <= muscle)
    {
        //  そんな筋肉はないため異常
        return -1;
    }

    …(処理)…
}

番兵を最後に必ず置いておくことで、範囲チェックを番兵を利用して行うテクニックです。

番兵に関して知りたい人はこちらの記事を参考にしてください。

ナナ
ナナ

これなら、筋肉の種類がいくら増えても範囲チェックの判定方法が変わらないんですね。将来に備えてプログラミングするのは大事なことです。