C言語編 第50章 列挙型

先頭へ戻る

この章の概要

この章の概要です。

列挙型

列挙型は、名前の付いた整数定数のリストによって定義される型です。

列挙とは、何かを「書き並べる」ことをいいます。列挙という言葉は、プログラミングをしているとよく出てくるので、ニュアンスを理解しておくといいです。

例えば、「ディレクトリ内のファイル名を列挙する」とか「文字列内の各文字を列挙する」というように使われます。

列挙型を使うにあたっては、構造体を使うときと同じく、まずは型の定義を行います。列挙型の定義は、次のように記述します。

enum タグ名 {
    列挙子,
    列挙子,
      :
};

列挙型を表すキーワードは enum です。

「タグ名」には、タグ(列挙型タグ)に付ける名前を記述します。構造体のタグと同じで、複数の列挙型を区別するために使う名前です。列挙型名を使うときには「enum タグ名」のように記述します。やはり構造体と同じで、typedef を使ってタグ名を指定せずに使えるようにできます(第26章)。

「タグ名」は省略することが可能です。ただし省略してしまうと、それ以降「enum タグ名」の形の記述ができなくなってしまうため、使い方が限定されます。

{ } の内側には、列挙子を並びます。列挙子は、列挙型に含まれる定数の名前(列挙定数)のことです。

後で取り上げますが、列挙定数は、その名前だけを書くこと場合もありますし、明示的に値を指定することもあります。このいずれの場合をも含めた用語が列挙子ということになります。

列挙定数は int型で表現できる範囲の整数値です。実際、列挙定数の型は int型です。

列挙定数には、自動的に値が割り振られます。先頭に記述した列挙定数には 0 が、後続の列挙定数には 1、2、3、… といった具合に順番に割り当てられます。

enum BloodType_tag {
    BLOOD_TYPE_A,   /* 0 */
    BLOOD_TYPE_B,   /* 1 */
    BLOOD_TYPE_AB,  /* 2 */
    BLOOD_TYPE_O    /* 3 */
};

4種類の血液型を表す列挙定数を定義しています。先頭に記述した BLOOD_TYPE_A には 0 が、BLOOD_TYPE_B には 1、BLOOD_TYPE_AB には 2、BLOOD_TYPE_O には 3 が割り当てられます。この例のように列挙型は、同じ意味合いを持ったもの(ここでは血液型)に属する定数値に名前を付けたいときに使います。

血液型のように、各列挙定数の具体的な値にはさほど興味がない場面というのはよくあります。単に、それぞれが異なる値を持っていてくれればそれでよいということですが、このような場面でも、列挙型はよく使われます。オブジェクト形式マクロで複数の定数を定義するよりも記述量が少ないですし、BloodType のような特定の型を明確に与えられる点でも優れています。

列挙型のタグ名や列挙定数の名前で、小文字や大文字をどう使い分けるかという点については、人それぞれ方針が違っています。オブジェクト形式マクロのルールに合わせて、列挙定数についてはすべて大文字にする人もいます。Microsoft の文書では、むしろタグ名の方がすべて大文字で、列挙定数は小文字になっています。ちなみに、標準規格の文書内では、タグ名も列挙定数名もすべて小文字で表記されています。

なお、列挙型そのものは、何らかの整数型(char型も含む)と一致する型です。どの型と一致するのかを決定するのはコンパイラの役目です。ルールとしては、すべての列挙定数の値がきちんと表現可能である型を選ぶということです。例えば、「enum BloodType_tag」の例では、列挙定数の範囲は 0~3 ですから、char型で十分だと判断して、char型を選ぶかも知れません。

列挙定数に明示的に値を与える

「列挙定数 = 定数式」のような表記で、明示的に任意の値を割り当てることが可能です。

enum Color_tag {
    RED = 5,     /* 5 */
    GREEN,       /* 6 */
    BLUE,        /* 7 */
    WHITE = 10,  /* 10 */
    BLACK        /* 11 */
};

明示的に値を与えた場合、その後続の列挙定数には、その続きの値が割り当てられます。この例では、RED は 5、GREEN は 6、BLUE は 7、WHITE は 10、BLACK は 11 です。

列挙定数は int型なので、負数を与えても構いません。また、同じ値を持った列挙定数ができても構いませんが、当然、それらの列挙定数を区別することができなくなることに注意する必要があります。

enum Color_tag {
    RED = -5,    /* -5 */
    GREEN,       /* -4 */
    BLUE,        /* -3 */
    WHITE = 10,  /* 10 */
    BLACK,       /* 11 */
    
    DEFAULT_COLOR = 10  /* 10 */
};

WHITE と DEFAULT_COLOR が同じ値になりますが、「標準の色」が「白」なのであれば、このような定義に問題はないでしょう。この場合、次のように書いた方が、重複が無くなるのでより良いです。

enum Color_tag {
    RED = -5,    /* -5 */
    GREEN,       /* -4 */
    BLUE,        /* -3 */
    WHITE = 10,  /* 10 */
    BLACK,       /* 11 */
    
    DEFAULT_COLOR = WHITE  /* 10 */
};

このように、列挙定数は、後続の列挙定数に与える初期値の指定で使うことが可能です。

末尾のコンマ

(C95規格までは)最後の列挙子の末尾にはコンマが置けません(コンパイラによっては許可されていることもあります)。

enum タグ名 {
    列挙子,
    列挙子,
      :
    列挙子,  /* この行の末尾の , は許されない */
};

C99 (末尾のコンマ)

C99 では、最後の列挙子の末尾に、コンマを置いてもよくなりました。

enum BloodType {
    BLOOD_TYPE_A,
    BLOOD_TYPE_B,
    BLOOD_TYPE_AB,
    BLOOD_TYPE_O,  /* この行の末尾の , は許される */
};

初期化

列挙型を定義したら、この型の変数を宣言することができます。

#include <stdio.h>

enum BloodType {
    BLOOD_TYPE_A,
    BLOOD_TYPE_B,
    BLOOD_TYPE_AB,
    BLOOD_TYPE_O
};

int main(void)
{
    enum BloodType btype = BLOOD_TYPE_AB;
    
    printf( "%d\n", btype );
};

実行結果

2

前述したとおり、列挙型自体の型は何らかの整数型です。初期値はその型に変換可能でなければなりません。普通は、いずれかの列挙定数をそのまま与えるか、同じ型の他の列挙型変数を与えることになるでしょう。

また、列挙型の定義と同時に変数を宣言することもできます。

#include <stdio.h>

enum BloodType {
    BLOOD_TYPE_A,
    BLOOD_TYPE_B,
    BLOOD_TYPE_AB,
    BLOOD_TYPE_O
} btype = BLOOD_TYPE_AB;

int main(void)
{
    printf( "%d\n", btype );
};

実行結果

2

このケースなら、タグ名を必要とする箇所が無いので、省略してしまえます。

#include <stdio.h>

enum {
    BLOOD_TYPE_A,
    BLOOD_TYPE_B,
    BLOOD_TYPE_AB,
    BLOOD_TYPE_O
} btype = BLOOD_TYPE_AB;

int main(void)
{
    printf( "%d\n", btype );
};

実行結果

2

基本的な使い方

実際のプログラムでの使い方を確認しましょう。色を表す列挙型を作り、その色の名前を配列で管理することを考えます。

#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <wchar.h>

#define COLOR_NUM  (5)  /* 色の総数 */

/* 色の列挙型 */
enum Color_tag {
    RED,
    GREEN,
    BLUE,
    WHITE,
    BLACK
};

/* 色の日本語表記 */
static const wchar_t* const COLOR_NAME_TABLE[COLOR_NUM] = {
    L"赤",
    L"緑",
    L"青",
    L"白",
    L"黒"
};

int main(void)
{
    enum Color_tag color;  /* 変数宣言の際にも enum が必要 */
    int i;

    /* LC_CTYPE をネイティブロケールに変更 */
    if( setlocale( LC_CTYPE, "" ) == NULL ){
        fputs( "ロケールの設定に失敗しました。\n", stderr );
        return EXIT_FAILURE;
    }


    /* 列挙型への代入 */
    color = BLUE;
    wprintf( L"%d: %ls\n", color, COLOR_NAME_TABLE[color] );
    color = 4;
    wprintf( L"%d: %ls\n", color, COLOR_NAME_TABLE[color] );

    /* 直接、列挙定数を使う */
    wprintf( L"%d: %ls\n", WHITE, COLOR_NAME_TABLE[WHITE] );

    /* すべて列挙する */
    for( i = 0; i < COLOR_NUM; ++i ){
        wprintf( L"%d: %ls\n", i, COLOR_NAME_TABLE[i] );
    }

    return 0;
}

実行結果

2: 青
4: 黒
3: 白
0: 赤
1: 緑
2: 青
3: 白
4: 黒

このサンプルプログラムのように、列挙定数を配列の添字として使うことがよくあります。その場合、配列の要素数の指定や、添字を for文で制御する必要が出てくると思いますが、いずれにしても、列挙定数は int型なので、特に問題なく使えます。

このように、整数型を要求する箇所で列挙定数を使うことができます。反対に、列挙型を求めているときに整数型の値を使うこともできますが、その列挙型定義に含まれていない値を使ってしまえる点には注意が必要です。

enum Color_tag color = 99;  /* 99 という値を持つ列挙定数は無いが可能 */

C++ では、列挙型変数へ整数型の値を代入するにはキャストが必要になりました(Modern C++編【言語解説】第2章)。

このサンプルプログラムでの列挙型の使い方は、少し改善できます。問題なのは、COLOR_NUM というマクロの存在です。このマクロの置換結果は、列挙子の個数を表していますが、この方法では、後から列挙子を増やしたり、減らしたりしたときに、忘れずに COLOR_NUM も書き換える必要があります。このような、「1箇所を書き換えたら、他の箇所も書き換えないといけない」という作りは、良いプログラムスタイルとは言えません

そこで、COLOR_NUM 自体も列挙子に含めてしまうという方法があります。

/* 色の列挙型 */
enum Color_tag {
    RED,
    GREEN,
    BLUE,
    WHITE,
    BLACK,

    COLOR_NUM   /* 色の総数 */
};

列挙定数の値は先頭を 0 として、順番に 1 ずつ増加するので、末尾に個数を表す列挙子を置くと、うまい具合に個数を意味する値が割り当てられます。この例だと、RED が 0、BLACK が 4 になり、COLOR_NUM には 5 が来ます。つまり「5種類の色がある」ということをきちんと表現できています。

この作りでは、新たに色を追加するときには、COLOR_NUM よりも手前側に列挙子を追加してやれば、COLOR_NUM の値が自動的に変更されます。色を減らすときも、その色を表している列挙子を削除すれば、自動的に COLOR_NUM の値も減らされます。

なかなかうまい方法ですが、唯一、COLOR_NUM という列挙定数を、色の定義の1つであるかのように扱ってしまうミスだけは気をつけないといけません。例えば、次のようなコードを書かないように注意が必要です。

wprintf( L"%d: %ls\n", COLOR_NUM, COLOR_NAME_TABLE[COLOR_NUM] );

COLOR_NAME_TABLE の要素数は COLOR_NUM個ですから、COLOR_NUM を添字にすると範囲外アクセスになってしまいます。

コンパイル時アサートによる改善

先ほどのサンプルプログラムは、コンパイル時アサート(第28章)を使って改善できます。

#define SIZE_OF_ARRAY(array) (sizeof(array)/sizeof(array[0]))
#define STATIC_ASSERT(exp)   typedef char static_assert_dummy[exp ? 1 : -1]

/* 色の日本語表記 */
static const wchar_t* const COLOR_NAME_TABLE[] = {
    L"赤",
    L"緑",
    L"青",
    L"白",
    L"黒"
};
STATIC_ASSERT( SIZE_OF_ARRAY(COLOR_NAME_TABLE) == COLOR_NUM );
/* 
ここでコンパイルエラーになったら、列挙型Color_tag の列挙子の個数と、
配列の要素数が一致していない。
*/

まず、配列COLOR_NAME_TABLE の要素数の指定を空にし、初期値の個数から自動判断させるように変更します。そして、その直後に、コンパイル時アサートを使って、要素数と COLOR_NUM の一致を確認します

こうすれば、列挙定数 COLOR_NUM と、配列 COLOR_NAME_TABLE の要素数の不一致が自動的に検出できるようになります。そのため、列挙子の個数を増やしたり減らしたりしたときに、配列 COLOR_NAME_TABLE の要素を更新し忘れたら、コンパイル時点できちんとエラーになります。

配列の要素数を意味する定数を定義して、それを使って配列宣言を行いがちですが、あえて要素数の指定を空にして、初期値の数で自動判断させると、コンパイル時アサートとの合わせ技が活用できます。要素数を明示的に指定してしまうと、初期値の個数が足りない場合、コンパイラが足りない分を補ってしまいエラーにならないため、検出できません。

記号定数との比較

オブジェクト形式マクロを使って定数を定義する方法は、整数型以外の定数が使えますが、列挙型では常に int型に限定されています。列挙型の方が限定的なので劣るようにも見えますが、型がはっきりしていることは利点になります。例えば、変数や仮引数、戻り値の型を単に int型とするよりも、enum Color型の方が意味合いがはっきりし、間違った値を使う危険性を多少減らす効果があります(残念ながら、コンパイルエラーにはなりませんが)。

また、列挙型の場合、スコープを限定することが可能です。例えば、関数内で列挙型の定義を行うと、その型は関数内でしか使えません。マクロでも、#undef を使って有効範囲を限定することはできますが(第24章参照)、忘れずに #undef を記述しないといけないため、利便性の面でも、安全性の面でも劣ります。

また、列挙型は列挙定数に具体的な値を指定しなくても、自動的に割り振られるという点も利点となるかも知れません。列挙型の定義の末尾に、列挙子の個数を表す定数を置いておくという方法が使えますし、コンパイル時アサートを使って、より堅牢なプログラムを実現できることも説明した通りです。マクロの場合、1つ1つに手動で値を割り当てないといけないため、後から新しい定数を追加したり、削除したりした場合、個数を表す定数も忘れずに修正する必要があります。

マクロは、プリプロセスの段階で置換されてしまうので、デバッグ時に意味のある名前が参照できない欠点もあります。つまり、デバッガの機能で変数の内容を確認したとき、列挙定数であれば、その名前が表示されるでしょう。マクロでは(デバッガにもよりますが)置換後の値しか表示されません。


練習問題

問題① 次のオブジェクト形式マクロを、1つの列挙型で定義して下さい。

#define AAA  (0)
#define BBB  (1)
#define CCC  (10)
#define DDD  (11)
#define EEE  (12)
#define FFF  (-1)


解答ページはこちら

参考リンク

更新履歴

'2018/5/8 全体的に整理と加筆。
項を細分化して、「列挙定数に明示的に値を与える」「末尾のコンマ」「初期化」を追加。
列挙型自体と列挙定数の型について補足。
列挙子と列挙定数を使い分けるように修正。
コンパイル時アサートによる改善」の項の一部を、第28章へ移動。

'2018/5/7 共用体とビットフィールドの話題を、第55章へ移動。
章のタイトルを変更(「列挙型、共用体、ビットフィールド」->「列挙型」)

'2018/4/5 VisualStudio 2013 の対応終了。

'2018/4/2 「VisualC++」という表現を「VisualStudio」に統一。

'2018/3/25 全面的に文章を見直し、修正を行った。

≪更に古い更新履歴を展開する≫





前の章へ(第49章 ビット演算)

次の章へ(第51章 日付と時間)

C言語編のトップページへ

Programming Place Plus のトップページへ


このエントリーをはてなブックマークに追加
rss1.0 取得ボタン RSS