WisdomSoft - for your serial experiences.

7.6 共用体

複数のメンバを共有する単一の値を共用体と呼びます。共用体は構造体と似ていますが、すべてのメンバは同じ領域を指しており、共用体のインスタンスはメンバの中で最も大きなサイズに合わせて作られます。単一の値を複数の型で表現したい場合に応用できます。

7.6.1 異なる型でメモリを共有

ポインタ型の変換をうまく利用し、ある型を異なる型のように振舞わせることができました。4つの int 型のメンバを持つ構造体のインスタンスは int 型のポインタにキャストすることで 4 つの要素を持つ int 型配列として扱うことができます。この事実は、データがメモリにどのように記録されているかという原理を知る重要な手がかりとなるでしょう。

この考えを発展させ、より実用的に振舞うのが共用体です。共用体の構文は極めて構造体に似ていますが、全てのメンバが同一の記録領域を共有するという点で、その性質が大きく異なっています。例えば数学における行列を想像してください。3次元グラフィックスプログラミングでは、頻繁に行列演算を行います。float 型で 4 × 4 の行列を実現させるには float matrix[4 * 4] でも float matrix[4][4] でも同じですし、行列の各要素を全て個別のメンバとして保有してもかまいません。どのような形式がよいかという議論の大部分はプログラマの好みの問題となるでしょう。共用体はこれらの問題を一度に解決してくれる有効な手段となります。

共用体は、構造体における struct の代わりに union キーワードを用います。

共用体の宣言
union タグ名{
	 メンバ名;
	...
} 共用体変数名;

タグ名を省略して無名共用体を作成できるという点でも構造体と同じです。共用体は、Pascal 言語経験者には可変レコードと類似した機能であると説明した方が分かりやすいかもしれません。共用体の全てのメンバは、同一のアドレスを返します。これは、共用体が同じ記憶領域を共有していることを証明しています。また、共用体のメンバへのアクセス方法は、構造体とまったく同じです。

コード1
#include <stdio.h>

union Value {
	unsigned char chValue;
	int iValue;
};

int main() {
	union Value u;
	u.iValue = 0xFFFF;

	printf(
		"chValue = %08X : &chValue = %p\n"
		"iValue = %08X : &iValue = %p\n" ,
		u.chValue , &u.chValue , u.iValue , &u.iValue
	);
	return 0;
}
実行結果
コード1 実行結果

コード1は、共用体 Value を宣言しています。この共用体は unsigned char 型のメンバ chValue と int 型のメンバ iValue を保有していますが、構造体とは異なり、これらのメンバは記憶領域を共有しています。そのため、実行結果から確認できるように、chValue メンバの値の変更は iValue メンバにも影響しますし、iValue メンバの変更は chValue メンバに影響を与えます。

まず、&chValue と &iValue が同じアドレスを返していることに注目してください。この結果から共用体が同一の記憶領域を共有していることが証明されています。共用体は、全てのメンバを共有するために、最もサイズの大きいメンバ型に合わせて記憶領域を確保します。コード1の Value 構造体は int 型にあわされます。chValue は確保されている int 型の記憶領域のうち、下位1バイトを共有しているのです。そのため、共用体変数 u が保持している値は 0xFFFF ですが、chValue でアクセスした場合は下位1バイトしか参照できないため 0xFF が返されます。

共用体は、構造体同様に構造体や共用体、配列のような複雑な型をメンバとして保有することも可能です。例えば、次のような複雑な型を作成することも可能です。

union {
	struct {
		float _11, _12, _13, _14;
		float _21, _22, _23, _24;
		float _31, _32, _33, _34;
		float _41, _42, _43, _44;
	} matrix ;
	float m[4][4];
} u ;

この共用体は、メンバに16個の float 型メンバを持つ構造体と、4 × 4 個の要素を持つ float 型2次元配列を宣言しています。構造体と配列は共に 16 個の float 型要素を保有するという点から、これらのメンバが必要とする記憶領域のサイズは同じです。開発者は構造体の各メンバにアクセスすることもできますし、配列から添字を指定してアクセスすることも可能です。このような共用体は、3次元グラフィックスの座標変換を行うための行列に使われます。構造体のメンバへは u . matrix . _11 というように外側から順番にメンバ指定します。

7.6.2 共用体の初期化

共用体の初期化は、構造体や配列のようにそれぞれの要素に行うことはできません。なぜならば、共用体のメンバは記憶領域を共有しているため、共用体が保有する要素の実体は 1 つであると考えられるためです。そこで、共用体の初期化は最初のメンバ型でのみ初期化を行うことができます。

コード2
#include <stdio.h>

union Point {
	struct {
		short int x , y;
	} point;
	int location;
};

int main() {
	union Point u = { 100 , 50 };

	printf(
		"x = %d : y = %d\nlocation X = %d : location Y = %d\n" ,
		u.point.x , u.point.y , (short int)u.location , u.location >> 16
	);
	return 0;
}
実行結果
コード2 実行結果

このプログラムの Point 共用体は、32 ビットコンピュータ上のシステムにおいて、上位 16 ビットと下位 16 ビットに論理的に分割された 32 ビットにパックされた座標を表すという仕様に適しています。この共用体の最初のメンバは short int 型の x と y を保有する無名構造体です。そのため、初期化には short int 型の二つの整数を指定することができます。これは、開発者が座標を指定する場合は直観的な仕様なので歓迎されるでしょう。

しかし、システムは都合上 32 ビット値として一括して扱う方が高速に処理できるかもしれません。その場合は location メンバにアクセスすればよいのです。このように、共用体は使い方によって八方美人になることができます。ただし、コード2は int 型が 32 ビット、short int 型が 16 ビットであることを想定しています。それ以外の環境では正しい結果を表示しないので注意してください。