共用体(union)は、複数のメンバーが同じメモリ領域を共有するC言語の型です。
メモリ使用量を抑えながら状況に応じて値の型を切り替えたいときに有効です。
本記事では、定義方法からサイズと配置のルール、メリットと注意点、実用例までを初心者向けに丁寧に解説します。
共用体(union)の基礎
共用体とは?メモリを共有する型
共用体は、複数の異なる型のメンバーをひとつにまとめ、それらが同じメモリ領域を共有する仕組みです。
構造体(struct
)と違い、同時に複数のメンバーを保持することはできません。
ある時点で有効なのは最後に書き込んだメンバーだけです。
この性質により、最大サイズのメンバー分のメモリだけを確保すればよいため、省メモリに役立ちます。
たとえば「数値か文字列かのどちらか」を保持するようなケースで特に有効です。
unionの定義と書き方
共用体はunion
キーワードで定義します。
以下は整数、浮動小数点、バイト配列を共有する例です。
// 共用体の基本定義と使用例
#include <stdio.h>
union Number {
int i; // 整数(典型的に4バイト)
float f; // 単精度浮動小数点(典型的に4バイト)
unsigned char bytes[4];// 生のバイト列(4バイト)
};
int main(void) {
union Number n;
// 整数として使う
n.i = 42;
printf("n.i = %d\n", n.i);
// 浮動小数として使う(以後、iの値は意味を持たない)
n.f = 3.5f;
printf("n.f = %f\n", n.f);
// バイト配列として使う(以後、fの値は意味を持たない)
n.bytes[0] = 0xAA;
n.bytes[1] = 0xBB;
n.bytes[2] = 0xCC;
n.bytes[3] = 0xDD;
printf("n.bytes = %02X %02X %02X %02X\n",
n.bytes[0], n.bytes[1], n.bytes[2], n.bytes[3]);
return 0;
}
n.i = 42
n.f = 3.500000
n.bytes = AA BB CC DD
最後に代入したメンバーだけを読むのが正しい使い方です。
以後のセクションで詳しく説明します。
サイズ(sizeof)の決まりとメモリ配置
共用体のサイズはメンバーの中で最大のサイズ以上になります。
さらにアライメント(整列)の制約により、必要に応じて切り上げられます。
メンバーの開始アドレスはすべて同じで、まさに「重なって」配置されます。
// サイズとアドレス配置を確認するプログラム
#include <stdio.h>
union Mix {
char c;
int i;
double d;
};
int main(void) {
union Mix u;
printf("sizeof(char) = %zu\n", sizeof(char));
printf("sizeof(int) = %zu\n", sizeof(int));
printf("sizeof(double) = %zu\n", sizeof(double));
printf("sizeof(union Mix) = %zu\n", sizeof(union Mix));
printf("_Alignof(union Mix) = %zu\n", (size_t)_Alignof(union Mix));
// すべてのメンバーのアドレスは同じになる
printf("&u.c = %p, &u.i = %p, &u.d = %p\n",
(void*)&u.c, (void*)&u.i, (void*)&u.d);
return 0;
}
sizeof(char) = 1
sizeof(int) = 4
sizeof(double) = 8
sizeof(union Mix) = 8
_Alignof(union Mix) = 8
&u.c = 0x7ff... , &u.i = 0x7ff... , &u.d = 0x7ff...
出力は環境によって異なりますが、unionのサイズは最大メンバー(ここではdouble)のサイズに一致し、アドレスが一致していることが分かります。
structとの違い
structは各メンバーが独立してメモリを持ち、unionはメモリを共有します。
そのため、structは全メンバー分のサイズが必要ですが、unionは最大メンバー分だけ確保します。
以下に簡単な比較表を示します。
項目 | struct | union |
---|---|---|
メモリ配置 | 各メンバーが別々に連続配置 | 全メンバーが同じ先頭アドレスを共有 |
同時に保持できる値 | すべてのメンバー | 最後に書いた1つだけ |
サイズ(sizeof ) | 各メンバー合計(+パディング) | 最大メンバー(+アライメント調整) |
用途 | 論理的に同時に存在する情報 | どれか1つだけ使う可変データ |
読み書きの安全性 | どれでも自由に読める | 最後に書いたメンバー以外は読まない |
サイズの差をコードで確認してみます。
// structとunionのサイズ比較
#include <stdio.h>
struct S {
int i;
float f;
unsigned char bytes[4];
};
union U {
int i;
float f;
unsigned char bytes[4];
};
int main(void) {
printf("sizeof(struct S) = %zu\n", sizeof(struct S));
printf("sizeof(union U) = %zu\n", sizeof(union U));
return 0;
}
sizeof(struct S) = 12
sizeof(union U) = 4
上記は一例です。
環境によりstruct S
が12や16などになることがありますが、unionは最大メンバーのサイズ(ここでは4)になる点がポイントです。
共用体(union)の使い方
メンバーの宣言とアクセス方法
共用体の変数は通常の変数と同じく宣言し、ドット演算子(.)でメンバーにアクセスします。
ポインタからアクセスする場合は->
演算子を使います(ポインタは本稿では深掘りしません)。
// メンバーの宣言とアクセス
#include <stdio.h>
union Value {
int i;
float f;
};
void print_value_as_int(union Value v) {
// v.iに最後に書いてあるときのみ意味を持つ
printf("as int: %d\n", v.i);
}
int main(void) {
union Value v;
v.i = 100; // intとして書く
print_value_as_int(v);// intとして読む(OK)
v.f = 1.25f; // floatとして書く
// ここでv.iを読むのは未定義動作なのでやらない
printf("as float: %f\n", v.f); // floatとして読む(OK)
return 0;
}
as int: 100
as float: 1.250000
初期化の基本
共用体は指定子付き初期化子(designated initializer)が分かりやすいです。
どのメンバーを有効にするかを明示できます。
// 共用体の初期化方法
#include <stdio.h>
union Number {
int i;
float f;
unsigned char b[4];
};
int main(void) {
union Number a = { .i = 123 }; // intで初期化
union Number b = { .f = 2.5f }; // floatで初期化
union Number c = { .b = {0xDE, 0xAD, 0xBE, 0xEF} }; // バイト列
printf("a.i = %d\n", a.i);
printf("b.f = %f\n", b.f);
printf("c.b = %02X %02X %02X %02X\n", c.b[0], c.b[1], c.b[2], c.b[3]);
return 0;
}
a.i = 123
b.f = 2.500000
c.b = DE AD BE EF
なお{0}
のように書くと、最初のメンバーでゼロ初期化されます。
どのメンバーを使うか明確にするため、指定子付き初期化子を使うのがおすすめです。
メリットと用途
共用体の最大のメリットはメモリの節約です。
たとえばセンサー値が「整数または浮動小数または短い文字列」のいずれかで届くとき、unionなら最大のメンバー分だけを確保すれば済みます。
また通信プロトコルの「可変ペイロード」や、ファイルフォーマットの「タグによって内容が変わるレコード」を表すのにも向いています。
一方で、最後に書いたメンバー以外を読むと未定義動作になる点は強い制約です。
後述する「タグ付き共用体」で、現在どのメンバーが有効かを管理すると安全です。
共用体の注意点
最後に書いたメンバーだけを読む
共用体は最後に代入したメンバーだけが有効です。
別のメンバーを読むと未定義動作で、環境によりたまたまそれらしい値が出ることもあれば、全くのゴミ値になることもあります。
// 悪い例: 最後に書いたメンバーと異なるメンバーを読む(未定義動作)
union Bad {
int i;
float f;
};
void bad_example(void) {
union Bad x;
x.i = 0x3F800000; // floatの1.0fのビット列を"意図"しているが...
// 次の1行は未定義動作(移植性なし、危険)
// printf("%f\n", x.f);
(void)x; // コンパイル警告回避のダミー
}
このような「型の再解釈」はやらないでください。
どうしてもビット列として扱いたいときは、次節のようにmemcpy
で安全にコピーします。
型の再解釈は非推奨
unionによる型の再解釈(type punning)はCの規格上未定義動作または実装依存とされ、移植性がありません。
代わりにmemcpyでビット列を明示的にコピーします。
// 安全なビット解釈: memcpyを使う
#include <stdio.h>
#include <stdint.h>
#include <string.h>
int main(void) {
float f = 1.0f;
uint32_t bits = 0;
// floatのビット列をu32に安全にコピー
memcpy(&bits, &f, sizeof(bits));
printf("float(1.0f) as u32 bits = 0x%08X\n", bits);
// 逆方向も同様にmemcpyを使う
float g = 0.0f;
memcpy(&g, &bits, sizeof(g));
printf("restored float = %f\n", g);
return 0;
}
float(1.0f) as u32 bits = 0x3F800000
restored float = 1.000000
memcpyなら最適化されてオーバーヘッドがほぼゼロになることが多く、安全性と移植性を両立できます。
アライメントとパディングに注意
共用体のアライメントは含まれるメンバーの中で最も厳しいものに従います。
つまり、doubleやlong longなどの高アライメントな型が含まれると、共用体全体の整列要求が上がる点に注意が必要です。
共用体を構造体に埋め込むと、共用体の前にパディングが入ることがあります。
// アライメントとオフセットの確認
#include <stdio.h>
#include <stddef.h> // offsetof
struct WithUnion {
char tag; // 1バイト
union {
int i; // 4バイト整列
double d;// 8バイト整列(これに引きずられる)
} u;
};
int main(void) {
printf("sizeof(struct WithUnion) = %zu\n", sizeof(struct WithUnion));
printf("_Alignof(struct WithUnion) = %zu\n", (size_t)_Alignof(struct WithUnion));
printf("offsetof(tag) = %zu\n", offsetof(struct WithUnion, tag));
printf("offsetof(u) = %zu\n", offsetof(struct WithUnion, u));
return 0;
}
sizeof(struct WithUnion) = 16
_Alignof(struct WithUnion) = 8
offsetof(tag) = 0
offsetof(u) = 8
この例では、union内のdoubleの影響で、共用体uは8バイト境界に配置され、char tagの後にパディングが入ることが分かります。
メモリマップやバイナリ互換性が重要な場面では、配置と境界に細心の注意を払いましょう。
初心者向けの実例とコツ
省メモリなデータ保持の例
「どれか1つだけ使う」データをunionにまとめると、インスタンスごとのメモリが節約できます。
以下は可変ペイロードの典型例です。
// 可変ペイロード: unionで省メモリ
#include <stdio.h>
struct MessageNaive {
int kind; // 種別(例: 1=int, 2=double, 3=text)
int as_int; // 使わないときも常に存在
double as_double;
char text[256];
};
struct MessageUnion {
int kind; // 種別(有効メンバーの識別)
union {
int as_int;
double as_double;
char text[256];
} payload; // どれか1つだけ使う
};
int main(void) {
printf("sizeof(MessageNaive) = %zu\n", sizeof(struct MessageNaive));
printf("sizeof(MessageUnion) = %zu\n", sizeof(struct MessageUnion));
return 0;
}
sizeof(MessageNaive) = 272
sizeof(MessageUnion) = 264
実際の差は環境に依存しますが、大きいメンバーを複数持つ場合はunionで大幅に削減できます。
とくに組込みや大量の要素を持つ配列では効果がはっきり表れます。
有効なメンバーをフラグで管理
共用体は「最後に書いたメンバーだけが有効」という制約があるため、現在どのメンバーが有効かを識別するフラグ(タグ)を必ず併用しましょう。
以下はシンプルな「タグ付き共用体」の例です。
ここでは分かりやすさのため整数のタグを使います(本来は列挙型が便利ですが詳細は別記事とします)。
// タグ付き共用体の基本パターン
#include <stdio.h>
#include <string.h>
#define TYPE_INT 1
#define TYPE_FLOAT 2
#define TYPE_TEXT 3
struct Value {
int type; // 有効メンバーの識別子
union {
int i;
float f;
char text[32];
} data;
};
void set_int(struct Value* v, int x) {
v->data.i = x;
v->type = TYPE_INT;
}
void set_float(struct Value* v, float x) {
v->data.f = x;
v->type = TYPE_FLOAT;
}
void set_text(struct Value* v, const char* s) {
// 安全のため長さ制限してコピー
strncpy(v->data.text, s, sizeof(v->data.text) - 1);
v->data.text[sizeof(v->data.text) - 1] = '// タグ付き共用体の基本パターン
#include <stdio.h>
#include <string.h>
#define TYPE_INT 1
#define TYPE_FLOAT 2
#define TYPE_TEXT 3
struct Value {
int type; // 有効メンバーの識別子
union {
int i;
float f;
char text[32];
} data;
};
void set_int(struct Value* v, int x) {
v->data.i = x;
v->type = TYPE_INT;
}
void set_float(struct Value* v, float x) {
v->data.f = x;
v->type = TYPE_FLOAT;
}
void set_text(struct Value* v, const char* s) {
// 安全のため長さ制限してコピー
strncpy(v->data.text, s, sizeof(v->data.text) - 1);
v->data.text[sizeof(v->data.text) - 1] = '\0';
v->type = TYPE_TEXT;
}
void print_value(const struct Value* v) {
switch (v->type) {
case TYPE_INT: printf("INT : %d\n", v->data.i); break;
case TYPE_FLOAT: printf("FLOAT: %f\n", v->data.f); break;
case TYPE_TEXT: printf("TEXT : \"%s\"\n", v->data.text); break;
default: printf("UNKNOWN TYPE\n"); break;
}
}
int main(void) {
struct Value v = {0};
set_int(&v, 42);
print_value(&v);
set_float(&v, 3.14f);
print_value(&v);
set_text(&v, "hello");
print_value(&v);
return 0;
}
';
v->type = TYPE_TEXT;
}
void print_value(const struct Value* v) {
switch (v->type) {
case TYPE_INT: printf("INT : %d\n", v->data.i); break;
case TYPE_FLOAT: printf("FLOAT: %f\n", v->data.f); break;
case TYPE_TEXT: printf("TEXT : \"%s\"\n", v->data.text); break;
default: printf("UNKNOWN TYPE\n"); break;
}
}
int main(void) {
struct Value v = {0};
set_int(&v, 42);
print_value(&v);
set_float(&v, 3.14f);
print_value(&v);
set_text(&v, "hello");
print_value(&v);
return 0;
}
INT : 42
FLOAT: 3.140000
TEXT : "hello"
書き込みの直後にタグを更新するのが安全のコツです。
読み出すときはタグを確認してから該当メンバーだけにアクセスします。
デバッグしやすい名前と設計
コード保守を楽にするため、以下の工夫をおすすめします。
箇条書きに頼りすぎず、理由もあわせて説明します。
デバッグ時に「どのメンバーが有効なのか」が一目で分かるように、タグの値とメンバー名の対応を明確にします。
タグはTYPE_INT
やTYPE_TEXT
のように自己説明的な定数名にし、共用体のメンバー名も用途が伝わる名前にします。
さらに、タグと値のセットを関数に閉じ込めることで、タグ更新漏れのバグを防げます。
// デバッグしやすい出力関数の例(タグとメンバー対応を明示)
#include <stdio.h>
#define TYPE_TEMP_C 10
#define TYPE_TEMP_F 11
struct Temperature {
int type;
union {
float celsius;
float fahrenheit;
} value;
};
void print_temperature(const struct Temperature* t) {
if (t->type == TYPE_TEMP_C) {
printf("Temperature: %.2f C\n", t->value.celsius);
} else if (t->type == TYPE_TEMP_F) {
printf("Temperature: %.2f F\n", t->value.fahrenheit);
} else {
printf("Temperature: UNKNOWN\n");
}
}
int main(void) {
struct Temperature t = {0};
t.type = TYPE_TEMP_C;
t.value.celsius = 23.5f;
print_temperature(&t);
t.type = TYPE_TEMP_F;
t.value.fahrenheit = 75.2f;
print_temperature(&t);
return 0;
}
Temperature: 23.50 C
Temperature: 75.20 F
「タグ更新と値更新をセットで行う」という設計ルールをチームで共有すると、ヒューマンエラーを減らせます。
まとめ
共用体(union
)は、メンバー同士が同じメモリ領域を共有し、最後に書いた1つだけが有効になるという特徴を持つ型です。
サイズは最大メンバーの大きさにアライメントを加味したもので、構造体(struct
)と比べて大幅にメモリを節約できるケースがあります。
ただし、最後に書いたメンバー以外を読む行為や、unionでの型再解釈は未定義動作で、移植性と安全性を損ねます。
ビットパターンを扱う必要があるときはmemcpyを使う、どのメンバーが有効かをタグで管理するなどの基本を押さえましょう。
アライメントとパディングの影響も理解しておくと、構造設計やメモリマップで困りません。
最後に、初心者の方は「使いたいときにだけ型を切り替える、同時に複数は保持しない」という発想で共用体を位置づけると理解が進みます。
用途が合えば強力な省メモリ手段になりますので、正しいパターン(タグ付き共用体、memcpyでのビットコピー)を身につけて安全に活用してください。