C言語編 第50章 列挙型、共用体、ビットフィールド

先頭へ戻る

このエントリーをはてなブックマークに追加

この章の概要

この章の概要です。

列挙型

導入

これまで、定数を作るためにはオブジェクト形式マクロ(第24章参照)を使っていました。

C++ ならば、const修飾子を利用することもありますが、C言語では「書き換えできない変数」にしかなりません。 (第37章参照

ここではもう1つの手段である列挙型を紹介します。

列挙型は、整数定数のリストを定義します。 記法としては、次のようになります。

enum タグ名 {
	定数,
	定数,
	  :
};
	

列挙型を表すキーワードは enum です。 列挙「型」と言っていることから分かるように、これは全体として1つの型になります。

構造体のときと同様、上記の記述は、新たな型を定義しただけであって、変数が宣言された訳ではありません。 列挙型の場合も、実際に使用する際には、変数を宣言します。

タグの意味も、構造体のときと同様です。 このタグ名を使って、他の列挙型との区別を付けます。

{ } の内側には、定義する定数の名前を書き並べます。 この「書き並べる」ことを、列挙と言います。 列挙という言葉は、C言語に限らず、プログラミングをしているとよく出てくるので、ニュアンスは知っておいた方がいいです。

例えば、ディレクトリ内のファイル名を列挙する。 文字列内の各文字を列挙する。という具合です。

列挙された定数(以下、列挙定数と呼びます)は、自動的に整数が割り振られます。 このとき、先頭に記述された列挙定数が 0 となり、その後は順番に 1、2、3 … といった具合に割り振られます
全体としては列挙型ですが、1つ1つの列挙定数は int型として扱われます。 そのため、整数型を要求する場所には、列挙定数を使うことができます。 逆に、列挙型の変数に対して、整数型の値を代入することもできます。

C++ では事情が異なり、列挙型変数へ整数型の値を代入するには、キャストが必要になります。 また、C++ では列挙定数の大きさが int型と一致する保証はありません。

なお、細かいところですが、最後の列挙定数の末尾にはコンマが置けません

enum タグ名 {
	列挙定数,
	列挙定数,
	  :
	列挙定数,  /* この行の末尾の , が許されるか? */
};
	

C99 (末尾のコンマ)

C99 では、末尾のコンマを置いてもよくなりました。

VisualC++、Xcode では、いずれも許可されています

基本的な使い方

以上のような使い方は、列挙定数の値が、具体的に幾つなのかには特に興味が無い場合に使えます。 例えば、色を表す定数リストを作り、その色の名前を配列で管理することを考えます。

#include <stdio.h>
#include <wchar.h>
#include <locale.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;

	setlocale( LC_CTYPE, "" );


	/* 列挙型への代入 */
	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: 黒
	

構造体のときと同様、列挙型の変数を宣言する際にも「enum」を付ける必要があります

C++ では、「enum」を付けることは必須ではありません。

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

このプログラムでの列挙型の使い方は、少し改善できます。 問題なのは、記号定数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 という列挙定数を、色の定義の1つであるように扱ってしまうミスだけは気をつけないといけません。 例えば、

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

これは駄目です。 ただ、意図的にこういうコードを書かない限り、普通は問題になりません。

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

また、もう一点。 列挙型とは直接関わりませんが、先ほどのサンプルプログラムにおいての配列の宣言も改善できます。

#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 の列挙定数の個数と、
  配列の要素数が一致していない。
*/
	

第28章で説明したアサートマクロの少し変わったバージョンです。 STATIC_ASSERT という名前で定義されたこのマクロは、不正なコードを発見すると、コンパイルの時点でそれを検出して、コンパイルエラーを起こさせます。 このタイプのアサートを、コンパイル時アサートや、静的アサートと呼びます。
仕組みとしては、マクロ内でダミーの配列型定義を行い、マクロに与えた引数に応じて、配列の要素数を変化させることで実現しています。 引数が真であれば、要素数を 1、偽であれば -1 とします。 要素数が負数というのはあり得ないことであり、コンパイルエラーになるので、これを利用している訳です。

この仕組みがあれば、列挙定数COLOR_NUM と、配列COLOR_NAME_TABLE の要素数の不一致が自動的に検出できるようになります。 まず、配列COLOR_NAME_TABLE の要素数の指定を空にし、初期値の個数から自動判断させるように変更します。 そして、その直後に STATIC_ASSERT を使って、要素数と COLOR_NUM の一致を確認します
配列の要素数に使える定数が手元にあると、それを使って配列宣言を行いがちですが、 あえて空にして、初期値の数で判断させる方法も、コンパイル時アサートとの合わせ技で非常に有用な手段になります。 要素数を COLOR_NUM で指定してしまうと、初期値の個数が足りない場合、足りない部分には自動的に 0 が当てはめられてしまうので、検出できないのです。

C11 (標準のコンパイル時アサート)

C11 になって、標準のコンパイル時アサートである _Static_assert が追加されました。

int main(void)
{
	_Static_assert(sizeof(int) == 4, "");

	return 0;
}
	

_Static_assert は標準機能なので、ヘッダのインクルードも必要ありません。 第1引数の条件式が偽になるとき、コンパイルが失敗し、 第2引数に指定した文字列を含んだエラーメッセージを出力します。

また、assert.h をインクルードすると、 static_assert という名前で使用できるようになります。

#include <assert.h>

int main(void)
{
	static_assert(sizeof(int) == 4, "");

	return 0;
}
	

VisualC++ 2013/2015/2017 のいずれも _Static_assert には対応していません。 static_assert については、VisualC++ 2013/2015/2017 のいずれでも使用できますが、 これは assert.h に含まれているものではなく、拡張機能として存在しているもののようです。
Xcode は、_Static_assert、static_assert のいずれにも対応しています。

列挙定数の値を指定する

列挙定数の値は、0 から始まる連番になるということでしたが、強制的に値を指定することも可能です。

enum タグ名 {
	列挙定数,
	列挙定数 = 値,
	  :
};
	

このように、「列挙定数 = 値」の形で記述すると、その列挙定数の値が強制的に指定できます。 例えば、

enum Color_tag {
	RED,
	GREEN = 5,
	BLUE,
	WHITE = 10,
	BLACK,
	
	COLOR_NUM   /* 色の総数…ではない */
};
	

このように定義した場合、RED は 0、GREEN は 5、BLUE は 6、WHITE は 10、BLACK は 11 になります。

ただし、こうして値を指定した場合、末尾に個数を表す列挙定数を置く手段が使えなくなることに注意が必要です。 実際、上の例では、COLOR_NUM の値は 12 になってしまいますから、正しくありません。

なお、同じ値が出来てしまっても問題ありませんし、負数を使っても構いません。

enum Color_tag {
	RED = 5,
	GREEN = 3,
	BLUE,
	WHITE,
	BLACK = -5
};
	

この場合、RED と WHITE がともに 5 になりますが、これでも構いません。

タグの省略と typedef

構造体と同様、列挙型の定義と同時に、変数の宣言も行う場合には、タグを省略できます(構造体の例は、第26章参照

enum {
	定数,
	定数,
	  :
} 変数名 = 初期値;
	

この場合、この列挙型の型名を表現する手段がないので、他の場所で型名を必要としている場合には使えません。

また、typedef を使って新しい型名を付けることによって、タグ名を省略することができる点も構造体と同様です。

typedef enum タグ名(省略可){
	定数,
	定数,
	  :
} 型名;
	

記号定数との比較

記号定数は、整数以外にも使えますが、列挙型は整数に限定されています。 列挙型の方が限定的なので劣るようにも見えますが、 型がはっきりしている列挙型の方が、コンパイラによる型チェックが働きやすくなるという利点があります

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

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

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

共用体

共用体は、1つのメモリ領域を、異なる型で共有するというものです。 これはかなり特殊な機能で、実際、他のプログラミング言語ではあまり見られないものです。

共用体の型定義は次のように行います。

union タグ名 {
	型 メンバ名
	型 メンバ名
	  :
};

共用体を表すキーワードは union です。
タグ名の説明は、構造体や列挙型のときと同じなので、詳細は割愛します、 これまで同様、変数宣言時には「union + タグ名」が必要ですし、それが嫌なら typedef が利用できる点も同様です。

C++ では、共用体変数の宣言時の union は省略可能です。

共用体の定義の構文は、構造体のものと同じように見えますが、意味合いは大きく異なります。 例えば、

struct S_tag {
	long   num;
	double d;
	char   str[10];
} s;

union U_tag {
	long   num;
	double d;
	char   str[10];
} u;

このように同じメンバで構成される構造体と共用体を定義したとき、メモリ上のイメージは次のようになります。

構造体のメモリイメージ 共用体のメモリイメージ

構造体の方は、各メンバが順番にメモリ上に配置されていきます。 一方、共用体の方は、先頭の位置が揃えられています。

構造体においては、アクセス効率を高めるため、実際にはメンバ間に、パディングと呼ばれる隙間が空けられることがあります。

このイメージは、メモリアドレスを出力してみることで、直接的に確かめられます。

#include <stdio.h>

struct S_tag {
	long   num;
	double d;
	char   str[10];
};

union U_tag {
	long   num;
	double d;
	char   str[10];
};


int main(void)
{
	struct S_tag s;
	union U_tag u;

	puts( "構造体の場合" );
	printf( "num: %p\n", &s.num );
	printf( "  d: %p\n", &s.d );
	printf( "str: %p\n", s.str );

	puts( "" );

	puts( "共用体の場合" );
	printf( "num: %p\n", &u.num );
	printf( "  d: %p\n", &u.d );
	printf( "str: %p\n", u.str );

	return 0;
}

実行結果

構造体の場合
num: 0017FF04
  d: 0017FF0C
str: 0017FF14

共用体の場合
num: 0017FEEC
  d: 0017FEEC
str: 0017FEEC

このように、共用体のメンバは同じアドレスにあります。 これはつまり、共用体のメンバは同時には使えないことを意味しています。
構造体と同じ構文なので、理解するまでは、変数が3個含まれていることを想像してしまいますが、そうではなくて、 共用体には1つの変数しか含まれておらず、型を取り替えて扱えるということです。 あるときは long型の値を扱い、またあるときには double型として扱うというような使い方ができます。

では、実際に使ってみます。

#include <stdio.h>
#include <string.h>

union Data_tag {
	int num;
	char c[4];
};


int main(void)
{
	union Data_tag data;

	data.num = 123;
	printf( "%d\n", data.num );

	memcpy( data.c, "abcd", 4 );
	printf( "%c%c%c%c\n", data.c[0], data.c[1], data.c[2], data.c[3] );

	return 0;
}

実行結果

123
abcd

共用体のメンバの参照は、構造体と同じように .演算子で行います。 ポインタ経由の場合には、->演算子が使える点も同様です

この例で、共用体Data_tag は 4Byte の大きさを持つというつもりで定義しました。 その 4Byte のメモリ領域を、int型としても扱えるし、要素数4 のchar型配列としても扱えるということです。
このような共用体の使い方をすると、整数と文字列が混在するようなデータ表を少ないメモリで実現できます。 共用体を使わない場合、例えば構造体で実現すると、一方のメンバを使った場合には他方のメンバはまったく未使用なままになってしまい、 無駄なメモリを使ってしまいます。

共用体の使い方として注意しなければならないのは、 最後に代入を行ったメンバからしか、正しい値は取得できる保証がないという点です。
例えば、data.num に代入した直後で data.c の値を調べると、どんな結果が返ってくるか分かりません。 同じメモリ領域を共有しているのだから、

data.num = 'a';
printf( "%c\n", data.c[0] );

と書けば、'a' が出力されるように思えますが、保証はありません。

なお、共用体変数を宣言時に初期化する場合は、先頭に書いたメンバの型でなければなりません。

union Data_tag data = 123;    /* 先頭のメンバは int型なので OK */
union Data_tag data = "abc";  /* 先頭のメンバは int型なので保証なし */

このように、最後にどこへ代入したかは意識しておく必要があります。 場合によっては、どこへ代入したかを別の変数に記憶させておいた方がいいかも知れません。 そういうときには、共用体定義を、構造体定義の内側に入れることがよくあります。
次のサンプルプログラムは、割と丁寧に関数化までしてあります。 じっくりお読み下さい。

#include <stdio.h>
#include <string.h>
#include <assert.h>

enum ValueType_tag {
	VALUE_TYPE_INT,     /* int型 */
	VALUE_TYPE_STRING,  /* char型配列 */

	VALUE_TYPE_NUM
};

struct Data_tag {
	enum ValueType_tag value_type;  /* 型 */

	union Value_tag {
		int num;
		char c[4];
	} v;
};

void setIntValue(struct Data_tag* data, int value);
void setStringValue(struct Data_tag* data, const char* value);
void printValue(const struct Data_tag* data);

int main(void)
{
	struct Data_tag data;

	setIntValue( &data, 123 );
	printValue( &data );

	setStringValue( &data, "abcd" );
	printValue( &data );

	return 0;
}

/*
	int型として値を格納する
	引数:
		data:	構造体のアドレス
		value:	格納する値
*/
void setIntValue(struct Data_tag* data, int value)
{
	data->v.num = value;
	data->value_type = VALUE_TYPE_INT;
}

/*
	文字列として値を格納する
	引数:
		data:	構造体のアドレス
		value:	格納する値。末尾の '\0' を含めず4文字でなければならない。
*/
void setStringValue(struct Data_tag* data, const char* value)
{
	assert( strlen(value) == 4 );

	memcpy( data->v.c, value, 4 );
	data->value_type = VALUE_TYPE_STRING;
}

/*
	現在の型に応じて正しい値を出力する
	引数:
		data:	構造体のアドレス
*/
void printValue(const struct Data_tag* data)
{
	switch( data->value_type ){
	case VALUE_TYPE_INT:
		printf( "%d\n", data->v.num );
		break;

	case VALUE_TYPE_STRING:
		printf( "%c%c%c%c\n", data->v.c[0], data->v.c[1], data->v.c[2], data->v.c[3] );
		break;

	default:
		assert( !"型が不適切です。" );
		break;
	}
}

実行結果

123
abcd

構造体のメンバとして、共用体定義とその変数宣言を含めています。 また、構造体のメンバには、列挙型変数が含まれています。
この構造体において、メモリ領域が共有されるのは、あくまでも共用体の中にある num と c であって、 列挙型変数の value_type は無関係であることに注意して下さい。

共用体変数への代入と、値の出力を関数化することで、常に列挙型変数value_type を使って適切なメンバが参照されるようになっています。 もちろん、常にこれらの関数を経由するようにプログラムを書かないといけませんが、それを守っていれば正常な状態が保たれるはずです。

列挙型の変数が加わったことによって、構造体全体のサイズが増えてしまうので、これではメモリの節約にはなりませんが、 共用体部分のサイズがもっと大きければ意味があります。

C99 (指示付きの初期化子)

C99 では、指示付きの初期化子という機能が追加されています。 これを使うと、特定のメンバを選んで初期値を与えることができます。

#include <stdio.h>

union Data_tag {
	int num;
	char c[4];
};


int main(void)
{
	union Data_tag data = { .c = "abcd" };

	printf( "%c%c%c%c\n", data.c[0], data.c[1], data.c[2], data.c[3] );

	return 0;
}

実行結果:

abcd

共用体変数 data の初期化のところを見て下さい。 「.c = "abcd"」という記述によって、c というメンバのところに初期値 "abcd" が与えられます。 このように、「.メンバ名 = 初期値」という構文が使えるようになりました。
C99 より前の規格では、共用体変数の宣言時に初期値を与える場合、先頭のメンバの型に合わせないと、 結果が保証されませんが、この機能によって、任意の型で初期化できます。

VisualC++、Xcode のいずれも対応しています。

ビットフィールド

ビットフィールドは、1Byte に満たない、ビット単位の領域を割り当てる仕組みです。 共用体同様、これもC言語以外のプログラミング言語では、なかなか見かけない特殊な機能です。

ビットフィールドは、構造体の定義の際に、メンバに対して、割り当てるビット数を併記するようにして記述します。

struct タグ名 {
	型 メンバ名 : ビット数;
	型 メンバ名 : ビット数;
	型 メンバ名;
	  :
};

ビット数の指定があるメンバと、指定のないメンバは混在しても構いません。

ビット数の指定を行う場合、そのメンバの型は、int型、signed int型、unsigned int型のいずれかでなければなりません。 もちろん、実際の領域の大きさは、ビット数のところに記述した数に制限されますから、sizeof(int) のサイズよりは小さくなります。 型が持つ意味は、符号の有無を指定することにあります。
int型と signed int型は、この場面に限っては異なる意味を持ちます。 signed int型を指定した場合は、必ず符号ありとなりますが、int型の場合に符号があるかないかはコンパイラに依存します

int型や unsigned int型以外を指定できるコンパイラもありますが、 重要なのは、符号の有無とビット数にあるので、あまり意味はありません。

ビット数のところには、0以上の整数を記述します。 ただし、0 を指定したときにどういう意味になるかは、コンパイラ依存となります。 また、sizeof(int) よりも大きなビット数を指定した場合の動作もコンパイラ依存です

ビットフィールドは、アドレスを取得できないことに注意して下さい。 アドレスは、Byte単位で割り振られているものなので、中途半端なビット位置から領域が確保される可能性があるビットフィールドでは、 アドレスが表現できないのです。 そのため、

struct Data {
	int a : 5;
	int b : 3;
	int c;
} data;

int* pb = &data.b;  /* エラー */
int* pc = &data.c;  /* OK */

このように、ビットフィールドである a や b に対して、&演算子は適用できません。


ビットフィールドの価値は、極限まで小さな領域にデータを詰め込める点に尽きます。 これはメモリが豊富でない環境では大きな価値がありますが、メモリが十分に足りている環境では、ほとんど無価値とも言えます。
メモリアドレスが Byte単位であることから分かるように、中途半端な位置にある変数をアクセスするのは、CPU に取っては楽なことではありません。 通常のメモリアクセスよりも、時間が掛かると考えて良いです。 なので、本当にメモリを節約する必要性があるときを除いて、無意味にビットフィールドを使うべきではありません。


練習問題

問題@ メンバの型や個数が同じ構造体と共用体と、それらの変数を宣言した後、それぞれの大きさを sizeof演算子で調べてみて下さい。 どのような違いがありますか?

問題A 1ビットのビットフィールドを1つだけ持つ構造体を作り、その構造体変数全体の大きさを sizeof演算子で調べてみて下さい。

問題B 次の記号定数を、1つの列挙型として定義して下さい。

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


解答ページはこちら

参考リンク

更新履歴

'2017/7/30 clang 3.7 (Xcode 7.3) を、Xcode 8.3.3 に置き換え。

'2017/6/10 「C11 (標準のコンパイル時アサート)

'2017/3/25 VisualC++ 2017 に対応。

'2016/10/15 clang の対応バージョンを 3.7 に更新。

'2015/12/27 SIZE_OF_ARRAYマクロの定義を修正。

'2015/10/12 clang の対応バージョンを 3.4 に更新。

'2015/9/5 VisualC++ 2012 の対応終了。

'2015/8/18 VisualC++ 2010 の対応終了。

'2015/8/15 VisualC++ 2015 に対応。

'2014/10/18 clang 3.2 に対応。

'2014/2/1 VisualC++ 2013 に対応。

'2014/1/25 setlocale関数の LC_CTYPE の指定を修正。

'2014/1/14 VisualC++ 2008 の対応終了。

'2014/1/12 clang 3.0 に対応。

'2013/7/24 列挙型の解説を細かく修正。

'2013/4/21 「C99 (指示付きの初期化子)」の項を追加。

'2013/4/14 列挙型定義の末尾のコンマについて、C99 での状況をまとめた。

'2011/3/12 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ