共用体(union)
複数のメンバが同じメモリ領域を共有するデータ構造です。異なる型として同じデータを解釈したり、メモリを節約したりする目的で使用します。サイズは最大のメンバのサイズに等しくなります。
構文
// 共用体型を定義します。
union 共用体タグ名 {
型 メンバ名1;
型 メンバ名2;
};
// 共用体変数を宣言します。
union 共用体タグ名 変数名;
// メンバへのアクセス(構造体と同じ記法)。
変数名.メンバ名;
ポインタ->メンバ名;
// 初期化は最初のメンバに対してのみ行えます(C89)。
// C99以降は指定初期化子が使えます。
union 共用体タグ名 変数名 = {.メンバ名 = 値};
union と struct の比較
| 項目 | struct | union |
|---|---|---|
| メモリ | 全メンバ分の合計(+パディング)を確保します。 | 最大メンバ1つ分のサイズのみ確保します。 |
| アクセス | 全メンバに独立してアクセスできます。 | 有効なのは最後に書き込んだメンバのみです。 |
| 用途 | 複数のデータをまとめて扱うことに使います。 | 同じメモリを異なる型で解釈することに使います。 |
サンプルコード
sample_union.c
#include <stdio.h>
#include <stdint.h>
/* 数値を異なる型として解釈する共用体です。 */
union Number {
int i;
float f;
unsigned char bytes[4];
};
/* タグ付き共用体: どのメンバが有効かを enum で管理します。 */
typedef enum { TYPE_INT, TYPE_DOUBLE, TYPE_STRING } ValueType;
typedef struct {
ValueType type;
union {
int i_val;
double d_val;
char s_val[64];
} data;
} Value;
void print_value(const Value *v) {
switch (v->type) {
case TYPE_INT: printf("int: %d\n", v->data.i_val); break;
case TYPE_DOUBLE: printf("double: %f\n", v->data.d_val); break;
case TYPE_STRING: printf("string: %s\n", v->data.s_val); break;
}
}
int main(void) {
union Number n;
n.i = 42;
printf("int値: %d\n", n.i);
/* float として書き込むと i の値は無効になります。 */
n.f = 3.14f;
printf("float値: %f\n", n.f);
/* bytes メンバでエンディアンを確認できます。 */
n.i = 1;
printf("bytes[0] = %u\n", n.bytes[0]); /* リトルエンディアンなら 1 */
printf("union Number のサイズ: %zu バイト\n", sizeof(union Number));
/* タグ付き共用体でバリアントデータを扱います。 */
Value v1 = {TYPE_INT, {.i_val = 100}};
Value v2 = {TYPE_DOUBLE, {.d_val = 3.14}};
print_value(&v1);
print_value(&v2);
return 0;
}
コンパイルして実行すると次のようになります。
gcc union.c -o union ./union int値: 42 float値: 3.140000 bytes[0] = 1 union Number のサイズ: 4 バイト int: 100 double: 3.140000
エンディアンを確認する
union を使うと、整数値のバイト順(エンディアン)を確認できます。組み込みやネットワークプログラミングでよく使われる技法です。
union_endian.c
#include <stdio.h>
#include <stdint.h>
union Endian {
uint32_t value;
unsigned char bytes[4];
};
int main(void) {
union Endian e;
e.value = 0x01020304;
printf("value = 0x%08X\n", e.value);
printf("bytes: [0]=%02X [1]=%02X [2]=%02X [3]=%02X\n",
e.bytes[0], e.bytes[1], e.bytes[2], e.bytes[3]);
if (e.bytes[0] == 0x04) {
printf("リトルエンディアン(下位バイトが先頭)\n");
} else {
printf("ビッグエンディアン(上位バイトが先頭)\n");
}
/* 32ビット float のビットパターンを uint32_t で見ます。 */
union { float f; uint32_t u; } fu;
fu.f = 1.0f;
printf("1.0f のビットパターン: 0x%08X\n", fu.u);
return 0;
}
コンパイルして実行すると次のようになります。
gcc union_endian.c -o union_endian ./union_endian value = 0x01020304 bytes: [0]=04 [1]=03 [2]=02 [3]=01 リトルエンディアン(下位バイトが先頭) 1.0f のビットパターン: 0x3F800000
よくあるミス
よくあるミス: 書き込んでいないメンバを読み出す
union では最後に書き込んだメンバだけが有効です。別のメンバを読み出すと未定義動作になります(unsigned char 経由のバイト読み出しは例外)。
union_undef_ng.c
#include <stdio.h>
union Data {
int i;
float f;
};
int main(void) {
union Data d;
/* NG: float を書き込んだあと int を読み出す(未定義動作) */
d.f = 3.14f;
/* printf("%d\n", d.i); */ /* 未定義動作: 書き込んでいない i を読む */
/* OK: 書き込んだメンバだけを読み出す */
d.i = 42;
printf("int: %d\n", d.i); /* OK: i を書いて i を読む */
d.f = 2.71f;
printf("float: %f\n", d.f); /* OK: f を書いて f を読む */
return 0;
}
修正後は次の通りです。
gcc union_undef_ng.c -o union_undef_ng ./union_undef_ng int: 42 float: 2.710000
概要
共用体の最大の特徴は、複数のメンバが同じメモリアドレスを共有することです。これにより1つの変数を複数の型として解釈できます。ハードウェアのレジスタやネットワークパケットの解析など、低レベルなバイト操作でよく使われます。
最後に書き込んだメンバ以外を読み出すことは、C言語の規格上「未定義動作」です。ただし、バイト表現の読み出し(『unsigned char』メンバ経由)は規格で許可されています。複数の型を安全に扱いたい場合はタグ付き共用体のパターンを使用してください。
関連するデータ構造として、全メンバを独立させたい場合は『struct(構造体)』を使います。関連する定数を列挙する場合は『enum(列挙型)』を参照してください。
記事の間違いや著作権の侵害等ございましたらお手数ですがこちらまでご連絡頂ければ幸いです。