[C言語のはじめ方] Part19: 共用体(union)とメモリの共有

C言語

Step 4: 構造体・共用体・列挙体

共用体(union)とメモリの共有

このステップでは、データをまとめて扱うための新しい方法として「共用体(union)」を学びます。これまでに学んだ「構造体(struct)」と似ていますが、メモリの使い方に大きな違いがあります。この違いを理解することが、共用体を使いこなす鍵となります🔑。

1. 共用体(union)とは? 🤔

共用体(union)は、C言語において複数の異なるデータ型のメンバー(変数)を定義できる点では構造体(struct)と似ています。しかし、決定的な違いは「メモリ領域を共有する」という点です。

構造体では、定義された各メンバーがそれぞれ固有のメモリ領域を確保します。一方、共用体では、すべてのメンバーが同じメモリ領域を共有します。共用体全体のサイズは、その中で最も大きいサイズのメンバーに合わせて確保されます。

言葉だけでは少し分かりにくいかもしれませんね。まずは、共用体をどのように定義し、使うのかを見ていきましょう。

共用体の定義方法

共用体は `union` キーワードを使って定義します。構造体の `struct` と同じような形式です。


// データ型が異なるメンバーを持つ共用体の定義例
union Data {
  int i;
  float f;
  char str[20]; // このメンバーが最も大きいサイズを持つ可能性が高い
};

// 共用体変数の宣言
union Data data_variable;

上記の例では、`int` 型の `i`、`float` 型の `f`、そして `char` 型の配列 `str` という3つのメンバーを持つ `Data` という名前の共用体を定義しています。

メンバーへのアクセス

共用体のメンバーへのアクセスは、構造体と同じくドット演算子(`.`)を使います。ポインタの場合はアロー演算子(`->`)です。


#include <stdio.h>
#include <string.h> // strcpy関数を使うために必要

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

int main() {
  union Data data; // 共用体変数の宣言

  // int型のメンバーに値を代入して表示
  data.i = 10;
  printf("data.i : %d\n", data.i);

  // float型のメンバーに値を代入して表示
  data.f = 220.5f;
  printf("data.f : %f\n", data.f);

  // char配列のメンバーに文字列をコピーして表示
  strcpy(data.str, "C Programming");
  printf("data.str : %s\n", data.str);

  // 注意:この時点で data.i や data.f の値は保証されない!
  // 最後に代入された data.str のメモリ表現が、
  // int型やfloat型として解釈されるだけ。
  printf("After setting str, data.i : %d\n", data.i);
  printf("After setting str, data.f : %f\n", data.f);

  return 0;
}

このコードを実行すると、各メンバーに値を代入し、それを表示できているように見えます。しかし、最後の2つの `printf` の出力を見ると、`data.i` や `data.f` の値が、以前に代入した `10` や `220.5` ではない奇妙な値になっているはずです。これが「メモリの共有」による影響です。詳しく見ていきましょう。

2. メモリの共有:共用体の最大の特徴 ✨

共用体の核心は、すべてのメンバーが同じメモリアドレスから始まる領域を共有することです。確保されるメモリのサイズは、共用体内で定義されたメンバーのうち、最もメモリサイズが大きいメンバーのサイズになります(ただし、アライメント の影響で、それより少し大きくなることもあります)。

アライメント:CPUが効率よくメモリにアクセスできるように、データの配置アドレスを特定のバイト数(通常は2, 4, 8バイトなど)の倍数に揃える仕組みのこと。ここでは詳細に立ち入りませんが、`sizeof` で確認するサイズが、単純な最大メンバーサイズと異なる場合があることだけ覚えておきましょう。

先ほどの `union Data` の例で考えてみましょう。環境にもよりますが、一般的に `int` は4バイト、`float` は4バイト、`char str[20]` は20バイトのサイズだとします。この場合、`union Data` 全体のサイズは、最も大きい `char str[20]` に合わせて、少なくとも20バイトが確保されます。

重要なのは、`i`, `f`, `str` のすべてが、この確保された20バイト(以上)のメモリ領域の先頭アドレスから始まるということです。

メモリ共有の挙動を確認する

以下のコードで、`sizeof` 演算子を使って共用体と各メンバーのサイズを確認し、メモリ共有の挙動を見てみましょう。


#include <stdio.h>
#include <string.h>

union Data {
  int i;        // 通常 4 バイト
  float f;      // 通常 4 バイト
  char str[20]; // 20 バイト
};

int main() {
  union Data data;

  // 共用体全体のサイズと各メンバーのサイズを表示
  printf("Size of union Data: %zu bytes\n", sizeof(union Data));
  printf("Size of data.i:     %zu bytes\n", sizeof(data.i));
  printf("Size of data.f:     %zu bytes\n", sizeof(data.f));
  printf("Size of data.str:   %zu bytes\n", sizeof(data.str));

  printf("\n--- Memory Sharing Demonstration ---\n");

  // 整数を代入
  data.i = 97; // ASCIIコードで 'a'
  printf("Assigned data.i = %d\n", data.i);
  // 同じメモリ領域を文字列として見てみる(最初の1バイト)
  // data.i のメモリ表現の最初のバイトが 'a' (97) になるため
  printf("As string (first char): %c\n", data.str[0]);
  // floatとして見ると、意味不明な値になる可能性が高い
  printf("As float: %f\n", data.f);


  printf("\n");

  // 文字列を代入
  strcpy(data.str, "Hi"); // 'H', 'i', '\0' がメモリに書き込まれる
  printf("Assigned data.str = \"%s\"\n", data.str);
  // 同じメモリ領域を整数として見てみる
  // "Hi" のメモリ表現 ('H', 'i', '\0', ...) を整数として解釈する
  printf("As integer: %d\n", data.i);
  // floatとして見ると、やはり意味不明な値になる可能性が高い
  printf("As float: %f\n", data.f);

  return 0;
}

重要:共用体のルール

共用体において、有効な値を持つメンバーは常に最後に値を代入したメンバーのみです。上記のコードで `data.i` に値を代入した後、`data.str[0]` や `data.f` を読み取っていますが、これは C 言語の仕様上、厳密には未定義動作を引き起こす可能性があります(特に `data.f` のような異なる型の解釈)。学習目的でメモリ共有の概念を示すために例示していますが、基本的には最後に書き込んだメンバー以外のメンバーから値を読み出すべきではありません

このコードを実行すると、共用体のサイズが最も大きいメンバー(`str[20]`)のサイズ(またはアライメント調整後のサイズ)になっていることがわかります。そして、一方のメンバーに値を代入すると、同じメモリ領域を共有している他のメンバーの「見かけ上の」値が変わってしまう様子が観察できます。これは、同じメモリ上のビットパターンを、異なるデータ型として解釈しようとするためです。

3. 構造体(struct)との比較 🤔

共用体と構造体の最も重要な違いはメモリの扱いです。

構造体(struct)

  • 各メンバーは独立したメモリ領域を持つ。
  • 全体のサイズは、基本的に全メンバーのサイズの合計(+アライメントによるパディング)。
  • すべてのメンバーの値を同時に保持できる。
  • 例:複数の関連データをひとまとめにする(学生情報:学籍番号、名前、年齢など)。

共用体(union)

  • 全メンバーが同じメモリ領域を共有する。
  • 全体のサイズは、最も大きいメンバーのサイズ(+アライメント調整)。
  • 有効な値を持つメンバーは常に1つだけ(最後に代入されたもの)。
  • 例:同じメモリ領域を状況に応じて異なる型として扱いたい場合。
サイズの違いを確認するコード例

#include <stdio.h>

// 構造体の定義
struct MyStruct {
  int i;    // 4 bytes (assuming)
  char c;   // 1 byte (assuming)
  double d; // 8 bytes (assuming)
};

// 共用体の定義
union MyUnion {
  int i;    // 4 bytes (assuming)
  char c;   // 1 byte (assuming)
  double d; // 8 bytes (assuming)
};

int main() {
  printf("Size of struct MyStruct: %zu bytes\n", sizeof(struct MyStruct));
  // 期待されるサイズ: 4 + 1 + 8 = 13 ではなく、
  // アライメントにより 16 や 24 になる可能性が高い

  printf("Size of union MyUnion:  %zu bytes\n", sizeof(union MyUnion));
  // 期待されるサイズ: 最大メンバーである double の 8 バイト
  // (アライメントにより変わる可能性は低いことが多い)

  return 0;
}

このコードを実行すれば、構造体と共用体でメモリサイズが大きく異なることが明確にわかります。構造体はメンバーの合計(+α)、共用体は最大メンバーのサイズになります。

4. 共用体の使いどころ 💡

共用体の「メモリ共有」という特性は、特定の状況で役立ちます。

メモリ使用量の節約

複数のデータ型のうち、同時に1つの型しか使わないことが分かっている場合、共用体を使うことでメモリ使用量を節約できます。例えば、ある変数に整数が入ることもあれば、浮動小数点数が入ることもあるが、両方が同時に入ることはない、といった状況です。

特に、メモリ容量が非常に限られている組み込みシステムなどでは、このメモリ節約効果が重要になることがあります。


// 例:センサーからの値。整数で来るか、小数で来るか決まっているが、同時ではない。
union SensorValue {
  int int_val;
  float float_val;
};

int main() {
  union SensorValue sensor_data;
  int sensor_type = 0; // 0: int, 1: float (例)

  // ... センサーの種類を判別する処理 ...
  sensor_type = 1; // 仮にfloat型センサーとする

  if (sensor_type == 0) {
    // 整数値を取得して代入
    sensor_data.int_val = 123;
    printf("Sensor value (int): %d\n", sensor_data.int_val);
  } else {
    // 小数値を取得して代入
    sensor_data.float_val = 45.67f;
    printf("Sensor value (float): %f\n", sensor_data.float_val);
  }
  // この共用体は int(4バイト) と float(4バイト) のうち大きい方、
  // つまり 4 バイトで済む (アライメントによる変化がなければ)。
  // 構造体なら 4 + 4 = 8 バイト (以上) 必要。

  return 0;
}

ただし、現代の一般的なPCやサーバー環境ではメモリ量が豊富にあるため、単純なメモリ節約目的で共用体を使うメリットは限定的かもしれません。コードの可読性や安全性を考慮すると、構造体を使う方が適切な場合も多いです。

異なるデータ型としてのメモリ解釈(注意が必要)

共用体を使うと、あるデータ型のビットパターンを、別のデータ型として解釈できます。これは、低レベルなプログラミングや、特定のハードウェアレジスタを操作する際などに使われることがありますが、非常に注意が必要な使い方です。

例えば、浮動小数点数の内部表現(ビット列)を整数として調べたい場合などに使われることが考えられますが、これは型の内部表現(エンディアンなど)に強く依存するため、環境が変わると動作しなくなる(移植性が低い)コードになりがちです。

エンディアン:複数バイトで構成されるデータをメモリ上に格納する際のバイト順序(ビッグエンディアン、リトルエンディアン)。

また、先述の通り、最後に書き込んだメンバー以外のメンバーを読み出す行為は、多くの場合で未定義動作となるリスクがあります。安全な型変換には、通常のキャスト演算子などを用いるべきです。この目的での共用体の使用は、C言語のメモリ表現に関する深い理解がない限り避けるべきでしょう。

タグ付き共用体(Tagged Union / Variant)

共用体の「どのメンバーが現在有効か」を管理するために、構造体と組み合わせて使う「タグ付き共用体」というパターンがあります。これは、共用体自体と、現在どのメンバーが有効かを示す「タグ」情報(通常は整数型や列挙型)を保持するメンバーを、一つの構造体にまとめたものです。


#include <stdio.h>
#include <string.h>

// 共用体で扱うデータの種類を示す列挙型
typedef enum { TYPE_INT, TYPE_FLOAT, TYPE_STRING } DataType;

// 共用体本体
union Value {
  int i;
  float f;
  char s[50]; // 文字列用
};

// タグ付き共用体 (構造体でラップ)
typedef struct {
  DataType type; // 現在有効な型を示すタグ
  union Value value; // 値を保持する共用体
} TaggedValue;

// 値を設定し、表示する関数
void printValue(const TaggedValue* tv) {
  switch (tv->type) {
    case TYPE_INT:
      printf("Type: Integer, Value: %d\n", tv->value.i);
      break;
    case TYPE_FLOAT:
      printf("Type: Float,   Value: %f\n", tv->value.f);
      break;
    case TYPE_STRING:
      printf("Type: String,  Value: \"%s\"\n", tv->value.s);
      break;
    default:
      printf("Type: Unknown\n");
  }
}

int main() {
  TaggedValue val1, val2, val3;

  // 整数値を設定
  val1.type = TYPE_INT;
  val1.value.i = 100;
  printValue(&val1);

  // 浮動小数点数を設定
  val2.type = TYPE_FLOAT;
  val2.value.f = 3.14f;
  printValue(&val2);

  // 文字列を設定
  val3.type = TYPE_STRING;
  strcpy(val3.value.s, "Hello Union!");
  printValue(&val3);

  return 0;
}

この方法を使えば、「現在どのメンバーが有効か」という情報(`type` メンバー)に基づいて安全に共用体のメンバーにアクセスできます。共用体の弱点である「どのメンバーが有効かわからない」問題を克服する方法の一つです。

5. 共用体を使う上での注意点 ⚠️

  • 有効なメンバーは1つだけ: 共用体に値を書き込むと、それ以前に書き込まれていた他のメンバーの値は無効になります(メモリが上書きされるため)。最後に書き込んだメンバー以外を読み出すことは、基本的に避けるべきです(未定義動作のリスク)。
  • 型システムによる保護がない: コンパイラは、あなたが正しいメンバーにアクセスしているかを通常チェックしません。タグ付き共用体のような仕組みを自分で実装しない限り、間違ったメンバーにアクセスしてしまう可能性があります。
  • 移植性の問題: メモリの内部表現に依存するような使い方(例:ビットパターンを異なる型で解釈する)をすると、CPUアーキテクチャやコンパイラが異なると期待通りに動作しない可能性があります。
  • デバッグの難しさ: ある時点での共用体の値が、どのメンバーとして有効なのかを追跡するのが難しい場合があります。

6. まとめ

  • ✅ 共用体(union)は、複数のメンバーが同じメモリ領域を共有するデータ構造です。
  • ✅ 共用体のサイズは、最も大きいメンバーのサイズによって決まります。
  • ✅ 構造体(struct)は各メンバーが独立したメモリを持つのに対し、共用体は共有します。
  • ✅ 主な用途は、メモリ使用量の節約(同時に1つの型しか使わない場合)や、タグ付き共用体による多様なデータ表現です。
  • 最後に書き込んだメンバー以外へのアクセスは基本的に避けるべきであり、使い方には注意が必要です。

共用体は、構造体ほど頻繁に使われるものではありませんが、C言語のメモリ管理の仕組みを理解する上で非常に興味深い機能です。メモリをどのように効率的に、あるいは柔軟に使うかという視点を与えてくれます。

この知識を活かして、次のステップ「列挙型(enum)と状態管理」に進みましょう! 💪

コメント

タイトルとURLをコピーしました