🇯🇵 日本語 | 🇺🇸 English
広告スペース

C言語 共用体(union)入門

複数のメンバが同じメモリを共有するデータ型。構造体(struct)と対で理解する。

📖 このページで覚えること
✅ 最低限ここだけ覚える
  • union は全メンバが同じメモリを共有
  • サイズは最大メンバのサイズ
  • 同時に有効なのは1メンバだけ
⭐ 余裕があれば読む
  • エンディアン判定の定番技
  • tagged union (enum + union)
  • type punning と C11 規格
💪 初心者は1回で分からなくて普通
union は「同じメモリを複数のメンバが共有する」抽象度が高く、実務で使われる機会も少ないため、後回しでも大丈夫です。
再挑戦のステップ
  1. 構造体 を確実に理解してから戻る
  2. サイズが「最大メンバのサイズ」である理由を sizeof で実験
  3. エンディアン判定の例だけは覚えると実用的
  4. 理解できなくても先に進んでOK (必要になったら戻る)
💡 コツ: union の実用は「tagged union」と「型パンニング」。初学者は struct をマスターすれば十分。

共用体とは

共用体 (union) は複数のメンバが同じメモリ領域を共有する型です。サイズは「一番大きいメンバのサイズ」になります。同時に1つのメンバしか意味を持たないことに注意してください。
#include <stdio.h>

union Data {
    int    i;
    float  f;
    char   s[8];
};

int main(void) {
    union Data d;

    d.i = 42;
    printf("d.i = %d\n", d.i);        // 42

    d.f = 3.14f;                    // 書き換えた瞬間にd.iは無効
    printf("d.f = %f\n", d.f);        // 3.14
    printf("d.i = %d\n", d.i);        // 値はfloat表現のビット列

    printf("sizeof = %zu\n", sizeof(d));// 8 (一番大きいメンバ)
    return 0;
}
構造体との違い: structは全メンバを並べて持つ(合計サイズ)。unionは重ねて持つ(最大メンバのサイズ)。

構造体との違い(メモリ図解)

struct S { int a; int b; }

a
a
a
a
a
0-3
b
b
b
b
b
4-7
sizeof = 8バイト。a と b は独立して存在。

union U { int a; int b; }

a = b
a/b
a/b
a/b
a/b
0-3
sizeof = 4バイト。a を書き換えると b も変わる(同じ場所を指しているから)。

実験コード

#include <stdio.h>

struct S { int a; int b; };
union  U { int a; int b; };

int main(void) {
    struct S s = {10, 20};
    printf("struct: a=%d b=%d size=%zu\n", s.a, s.b, sizeof(s));
    // → a=10 b=20 size=8

    union U u;
    u.a = 10;
    u.b = 20;
    printf("union:  a=%d b=%d size=%zu\n", u.a, u.b, sizeof(u));
    // → a=20 b=20 size=4   (aもbも同じ場所!)
}

サイズとアラインメント

union のサイズは「最大メンバのサイズ」以上になります。アラインメント要件によりパディングが入ることも。
union Mix {
    char   c;       // 1バイト
    int    i;       // 4バイト
    double d;       // 8バイト
};
// sizeof(union Mix) は少なくとも 8バイト
// アラインは double の要求に合わせて 8

union で同じ領域を別の視点で見る

union Split {
    int           n;      // 4バイトをまとめて
    struct { char b0, b1, b2, b3; } bytes;  // バイト単位
};

union Split s;
s.n = 0x12345678;
printf("%02x %02x %02x %02x\n",
       (unsigned)s.bytes.b0, (unsigned)s.bytes.b1,
       (unsigned)s.bytes.b2, (unsigned)s.bytes.b3);
// リトルエンディアンなら: 78 56 34 12
// ビッグエンディアンなら:   12 34 56 78

エンディアン判定の実例

「現在のCPUがリトルエンディアンかビッグエンディアンか」を union で簡潔に判定できます。これは教科書でも定番の応用例です。
#include <stdio.h>

int is_little_endian(void) {
    union { int i; char c[sizeof(int)]; } u;
    u.i = 1;
    return u.c[0] == 1;     // 先頭バイトが1なら LE
}

int main(void) {
    printf("%s-endian\n",
           is_little_endian() ? "little" : "big");
    return 0;
}

なぜ動くか

int i = 1 の4バイト配置:
リトルエンディアン (x86等): 01 00 00 00c[0] == 1
ビッグエンディアン (一部ARM等): 00 00 00 01c[0] == 0
union を使わない方法: int n = 1; char *p = (char*)&n; でも同じことができるが、unionの方がコンパイラが正しく扱える(ポインタ型変換より安全)とされる。

tagged union(variant パターン)

複数の型のどれか1つを入れるデータを作るとき、「どの型が入っているかを示すタグ」と「実データの union」を struct にまとめるのが定石です。他言語の sum type、Rustの enum に相当します。
#include <stdio.h>

enum ValueKind { V_INT, V_FLOAT, V_STRING };

struct Value {
    enum ValueKind kind;        // 「今どれが有効か」のタグ
    union {
        int   i;
        float f;
        char  s[32];
    } data;
};

void print_value(struct Value v) {
    switch (v.kind) {
        case V_INT:    printf("int: %d\n", v.data.i);    break;
        case V_FLOAT:  printf("float: %f\n", v.data.f); break;
        case V_STRING: printf("str: %s\n", v.data.s);   break;
    }
}

int main(void) {
    struct Value a = {V_INT, .data.i = 42};
    struct Value b = {V_FLOAT, .data.f = 3.14f};
    struct Value c;
    c.kind = V_STRING;
    snprintf(c.data.s, sizeof(c.data.s), "hello");

    print_value(a);  // int: 42
    print_value(b);  // float: 3.14
    print_value(c);  // str: hello
}
JSONパーサ、AST、メッセージ解析などで頻出するパターン。kind を必ずチェックしないとバグの温床になるので、ラッパー関数を作って隠すのが実用的。

サイズはどうなるか

// struct Value のサイズ:
//   enum (4) + 最大メンバ(32) + パディング = 36以上
// 32バイトの文字列を使わない場合でも常にこのサイズになる点に注意

落とし穴と使いどころ

注意点

使いどころ

  1. メモリ節約: 埋め込み機器で、どれか1種類しか使わないと分かっているデータ
  2. variant 型: JSON値、ASTノード、プロトコルメッセージ(tagged union と組み合わせ)
  3. 低レベルのメモリ表現確認: エンディアン判定、ビットパターン取得
  4. ハードウェアレジスタマッピング: 同じレジスタをビットフィールドと整数の両方で扱う
現代的な代替: 純粋なメモリ節約が目的ならvoid*とサイズの組、あるいはC++ の std::variant / Rust の enum の方が安全。Cでも tagged union + ラッパー関数で十分使える。

チャレンジ課題

課題1: float のビットパターンを見る
union { float f; uint32_t u; } を使い、f = 1.0f のときの 32bit 表現を16進数で表示せよ(IEEE 754 → 0x3F800000)。
課題2: エンディアン判定を関数にする
本文の is_little_endian を自分で書き、手元の環境で実行結果を確かめよ。Mac / Linux / Windows のほとんどはリトルエンディアンのはず。
課題3: シンプルな計算機の値型
整数・浮動小数・真偽値の3種類を持つ tagged union struct Value を作り、Value add(Value a, Value b) を実装。数値なら合計、真偽値同士なら OR を返す。型が不一致ならエラーメッセージを出す。
課題4: ビットフィールドとの組み合わせ
32bit 整数として扱うか、8bit × 4 のビットフィールドとして扱うかを選べる union を作り、RGBA 色 (0xRRGGBBAA) を分解・再合成するプログラムを書け。

関連ページ