共用体と列挙型

共用体は多用途充電ケーブルのようなものです。一度に使えるインタフェースは1つだけですが、空間を節約できます。列挙型はメニュー番号のようなものです。生の数値の代わりに名前を使うことでコードの可読性が上がります。

共用体の定義と共有メモリ

union のすべてのメンバは同じメモリを共有します。そのサイズは最も大きいメンバのサイズに等しくなります。同時に使えるメンバは1つだけです。

C
union Data {
    int i;
    float f;
    char str[4];
};

printf("%zu\n", sizeof(union Data));
TEXT
4

int メンバに代入した後、float メンバを読み取ると、同じメモリの異なる解釈が得られます:

C
union Data d;
d.i = 42;
printf("i = %d\n", d.i);
printf("f = %f\n", d.f);
TEXT
i = 42
f = 0.000000
⚠️ 注意: 最後に書き込んだメンバ以外を読み取る場合の動作は処理系定義です(プラットフォームと型に依存します)。実際には、現在どのメンバが有効かを区別するためにタグフィールドを使用してください。

共用体と構造体の比較

特徴 構造体 共用体
メモリ 各メンバが独自の空間を持つ すべてのメンバが同じ空間を共有
サイズ 全メンバのサイズの合計(アライメントパディング含む) 最も大きいメンバのサイズ
同時アクセス すべてのメンバに同時にアクセス可能 一度に1つのメンバのみ
変更の影響 1つの変更は他に影響しない 1つの変更はすべてに影響する
C
struct SData {
    int i;
    float f;
    char str[4];
};

union UData {
    int i;
    float f;
    char str[4];
};

printf("struct: %zu\n", sizeof(struct SData));
printf("union:  %zu\n", sizeof(union UData));
TEXT
struct: 12
union:  4

タグ付き共用体

実際には、共用体には通常、現在格納されている型を示すタグフィールドがペアになります:

C
typedef struct {
    int type;
    union {
        int int_val;
        float float_val;
        char str_val[32];
    } value;
} Variant;

void print_variant(const Variant *v) {
    switch (v->type) {
        case 0: printf("int: %d\n", v->value.int_val); break;
        case 1: printf("float: %f\n", v->value.float_val); break;
        case 2: printf("str: %s\n", v->value.str_val); break;
    }
}

C
#include <stdio.h>

typedef struct {
    int kind;
    union {
        int ival;
        double dval;
    } u;
} Number;

void print_number(const Number *n) {
    if (n->kind == 0) {
        printf("int: %d\n", n->u.ival);
    } else {
        printf("double: %.2f\n", n->u.dval);
    }
}

int main(void) {
    Number a = {0, .u.ival = 42};
    Number b = {1, .u.dval = 3.14};
    print_number(&a);
    print_number(&b);
    return 0;
}
▶ 試してみよう
TEXT
int: 42
double: 3.14

列挙型の定義と使い方

列挙型は整数定数の代わりに名前を使い、コードの可読性を向上させます。

C
enum Color {
    RED,
    GREEN,
    BLUE
};

enum Color favorite = GREEN;
if (favorite == GREEN) {
    printf("Green selected\n");
}
TEXT
Green selected

列挙値の指定

C
enum Weekday {
    MON = 1,
    TUE,
    WED,
    THU,
    FRI,
    SAT = 100,
    SUN
};

printf("MON=%d SAT=%d SUN=%d\n", MON, SAT, SUN);
TEXT
MON=1 SAT=100 SUN=101
💡 ヒント: 明示的な値を持たない列挙定数は、直前の値に+1した値を自動的に取ります。最初の定数のデフォルトは0です。

列挙型とswitch

C
enum Direction { UP, DOWN, LEFT, RIGHT };

void move(enum Direction d) {
    switch (d) {
        case UP:    printf("Move up\n"); break;
        case DOWN:  printf("Move down\n"); break;
        case LEFT:  printf("Move left\n"); break;
        case RIGHT: printf("Move right\n"); break;
    }
}
⚠️ 注意: 列挙型は int がベースです。任意の整数値を代入でき、コンパイラは警告しない場合があります。この機能を濫用しないでください。

列挙型の内部int値

列挙定数は本質的に int 型の名前付き定数です。

C
enum Status { OK = 200, NOT_FOUND = 404, ERROR = 500 };
printf("%d %d %d\n", OK, NOT_FOUND, ERROR);
TEXT
200 404 500

列挙変数は整数との間で代入できます(C言語の場合)が、型の一貫性のために列挙値を使うのが望ましいです:

C
enum Status s = OK;
int code = s;
💡 ヒント: 定数には #define より列挙型を優先してください。列挙型には型情報があり、デバッガでシンボル名が表示され、スコープも制御しやすいです。

共用体によるエンディアン検出

エンディアンとは、複数バイトのデータのメモリ上でのバイト順序のことです。共用体を使うと簡単に検出できます:

C
int is_little_endian(void) {
    union {
        int i;
        char c;
    } u;
    u.i = 1;
    return u.c == 1;
}

最下位バイトが最下位アドレスに格納されていれば(u.c == 1)、リトルエンディアンです。そうでなければビッグエンディアンです。

C
#include <stdio.h>

typedef union {
    unsigned int i;
    unsigned char bytes[4];
} EndianTest;

void print_byte_order(void) {
    EndianTest t;
    t.i = 0x12345678;
    printf("Memory byte order: ");
    for (int i = 0; i < 4; i++) {
        printf("%02X ", t.bytes[i]);
    }
    printf("\n");
    if (t.bytes[0] == 0x78) {
        printf("Little Endian\n");
    } else {
        printf("Big Endian\n");
    }
}

int main(void) {
    print_byte_order();
    return 0;
}
▶ 試してみよう
TEXT
Memory byte order: 78 56 34 12
Little Endian
🔥 よくある間違い: x86/ARMは通常リトルエンディアンですが、ネットワーク伝送ではビッグエンディアン(ネットワークバイトオーダー)が使われます。エンディアンの理解はプロトコル解析やファイルフォーマット処理に不可欠です。

共用体による実践的なメモリ節約

構造体に複数の相互排他的なフィールドがある場合、共用体を使って空間を節約できます:

C
typedef struct {
    int type;
    union {
        struct { int width, height; } rect;
        struct { int radius; } circle;
        struct { int side; } square;
    } shape;
} Figure;

Figure f;
f.type = 1;
f.shape.circle.radius = 10;

3つの図形のうち同時に使われるのは1つだけです。共用体がメモリを共有し、構造体のサイズは最も大きい図形で決まります。

❓ よくある質問

Q 共用体で複数のメンバを同時に使えますか?
A いいえ。共用体のメンバはメモリを共有しており、1つを書き込むと他を上書きします。最後に書き込んだメンバのみを読み取ってください。
Q 列挙型の名前を%sで出力できますか?
A いいえ。列挙値は整数です。名前を出力するにはマッピング関数や配列を書く必要があります。例:const char *names[] = {"RED","GREEN","BLUE"}
Q 定数に列挙型と#defineの違いは何ですか?
A 列挙型には型(int)があり、デバッガでシンボル名が表示され、スコープも制御しやすいです。#defineは型情報のないプリプロセッサ置換です。列挙型を優先してください。
Q エンディアン検出で共用体を使う方がポインタキャストよりよいのはなぜですか?
A 本質的には同じですが、共用体の方法はCの型安全性の精神により合致し、strict aliasing規則の問題を回避でき、可読性も高いです。

📖 まとめ

📝 練習問題

  1. unsigned int addrunsigned char bytes[4] を含む union IPAddress を定義し、IP整数値を入力して整数と4つのバイトの両方で出力してください
  2. 列挙型 enum Season { SPRING, SUMMER, AUTUMN, WINTER } を定義し、列挙値に基づいて対応する季節名文字列を返す関数を作成してください
  3. タグ付き共用体を使って型タグと共用体(長方形/円)を持つ簡単な図形構造体を実装し、面積を計算する関数を作成してください
100%