WisdomSoft - for your serial experiences.

7.7 列挙型

列挙型を用いることで、容易に任意の整数に対応する識別子を作成できます。複数の項目からなる定数のリストを作成する場合に便利です。

7.7.1 名前付き定数

実践のプログラムでは、関数やシステムに情報の属性などを伝える手段として定数を与えることがあります。定数自身に意味は存在せず、設計者が定数に対応する意味を付けます。例えば 2 が渡されれば OK、4 が渡されればキャンセル、8 ならば再試行、というような具合です。

こうした定数を要求する場合、定数を直接コードに記述するより、定数に名前をつけて抽象的に表現した方が柔軟性があると設計者は考えることでしょう。そこで、ひとつの手段として列挙型を使います。

列挙型は、列挙子と呼ばれる名前付き定数の集合として構成されます。列挙子は、定数を指定することができるあらゆる場所で指定することが可能です。基本的には、論理的な意味のある定数の集合を一意の名前として抽象表現したい場合に有効な方法となります。列挙を使う場合、開発者は列挙子が持つ定数ではなく、列挙子が持つ意味に注目しなければなりません。列挙子の意味論はシステム設計者に委ねられます。

列挙型は enum キーワードを用いて宣言します。 構文や扱い方は構造体に似ているので難しいものではありません。

列挙型の宣言
enum タグ名 {
	列挙子1 = 定数 , 列挙子2 = 定数 ...
} 列挙変数 ;

タグ名と末尾の列挙変数の宣言は、構造体や共用体と同じように省略することができます。列挙子は定数に与える識別子です。これには C 言語の識別子命名規則が適応されますが、習慣的に全て大文字で記述します。C 言語などの大文字と小文字を区別する言語において、定数や変更不可能な静的変数の識別しには大文字のみを使うという習慣があります。これは、他の変数などの識別子と区別するためです。

定数は、列挙子が保有する定数を指定します。ここには数値定数、または文字定数を指定することができます。列挙子の定数は省略することが可能であり、定数が省略されている場合は、その前の列挙子の値をインクリメントした値が自動的に設定されます。最初の列挙子の定数が省略されている場合は 0 が与えられます。

enum Message { MSG_YES , MSG_NO };

例えば、この列挙は、MSG_YES は 0 を、MSG_NO は 1 を表す定数として扱うことができます。

enum Message { MSG_YES , MSG_NO , MSG_OK = 8 , MSG_CANCEL };

しかし、この場合は MSG_OK に 8 という定数を明示的に与えているため、MSG_CANCEL は 9 を表します。列挙型の変数を作成することはできますが、この変数は実質上は int 型にすぎません。列挙子は構造体のメンバとは性質が異なります。列挙子はメンバではなく、単純に定数の別名であると捉えるべきです。

コード1
#include <stdio.h>

enum Message { MSG_OK , MSG_YES = 2 , MSG_NO };

int main() {
	enum Message msg = MSG_NO;
	printf(
		"MSG_OK = %d : MSG_YES = %d : MSG_NO = %d\n" ,
		MSG_OK , MSG_YES , msg
	);
	return 0;
}
実行結果
コード1 実行結果

コード1では列挙型 Message を作成しています。この列挙型は3つの列挙子 MSG_OK、MSG_YES、MSG_NO を持っています。実行結果を見ると、列挙型の宣言で MSG_YES には 2 という定数を明示的に指定しているため、MSG_NO はこれに影響されて 3 という値を表していること確認できます。このように、列挙は定数を省略することができるため、定数自体ではなく、定数に与える論理的な意味に興味がある場合は非常に有効です。

コード1を見て疑問に思うことがいくつかあることでしょう。まず、列挙型の msg 変数の存在です。この変数には初期化時に MSG_NO を与えていますが、もしこれ以外の値を与えたらどうなるのでしょうか。実は、列挙型変数の実体は単純な int 型です。列挙子以外を与えても、エラーになることはありませんし、コンパイラはそれを監視しません。

enum Message msg = 100;

このように記述しても、コンパイラは何事もないようにコンパイルを通します。また、構造的な設計を重視するプログラマは Message.MSG_OK というような定数へのアクセスを好むかもしれませんが、残念ながらこれもできません。列挙子はメンバではないため、構造体のようなアクセスはできないのです。この仕様は奇妙だと感じてしまいますが、列挙変数に代入される値を実行時にチェックしてしまえば、速度を重視する C 言語の基本方針に反してしまいます。与えられた定数が正しいものかどうかを調べるのは開発者の役目とされます。

コード1は列挙を使った簡単な実践例です。多くの C 言語で作成されたシステムは、このように定数に論理的な意味を定めて情報の属性や挙動の要求などを行うことができます。

コード2
#include <stdio.h>

enum { MSG_OK , MSG_YESNO };
enum { ID_OK = 1 , ID_YES , ID_NO };

int Message(char *msg , int type) {
	char ch;
	switch(type) {
	case MSG_OK:
		printf("%s\tPush Enter>" , msg);
		scanf("%c" , &ch);
		return ID_OK;
	case MSG_YESNO: 
		printf("%s y/n>" , msg);
		scanf("%c" , &ch);
		return (ch == 'y' ? ID_YES : ID_NO);
	}
	return 0;
}

int main() {
	Message("Stand by Ready!" , MSG_OK);
	if (Message("Are you sure that's enough aromr?" , MSG_YESNO) == ID_YES)
		printf("This is not your appointed time to die.\n");
	return 0;
}
実行結果
コード2 実行結果

このプログラムの Message() 関数は、ユーザーに指定した何らかの文字列をメッセージとして表示し、入力を待機します。type 引数が MSG_OK の場合は入力を待ちますが、入力された値は評価せずに ID_OK を常に返します。type 引数に MSG_YESNO を指定すると y または n の入力を促し、ユーザーに選択を行わせます。ユーザーが入力した結果に応じて、関数は ID_YES または ID_NO を返します。

メッセージを表示するという目的は統一されているため、こうした機能は Message() というひとつの関数にまとめ、定数でその動作を分岐させる方法がもっともスマートだと考えられます。基本的な関数の挙動が同一であるにもかかわらず分割してしまうのはスマートではありません。