Step 4: 構造体・共用体・列挙体
共用体(union)とメモリの共有
このステップでは、データをまとめて扱うための新しい方法として「共用体(union)」を学びます。これまでに学んだ「構造体(struct)」と似ていますが、メモリの使い方に大きな違いがあります。この違いを理解することが、共用体を使いこなす鍵となります。
1. 共用体(union)とは?
共用体(union)は、C言語において複数の異なるデータ型のメンバー(変数)を定義できる点では構造体(struct)と似ています。しかし、決定的な違いは「メモリ領域を共有する」という点です。
構造体では、定義された各メンバーがそれぞれ固有のメモリ領域を確保します。一方、共用体では、すべてのメンバーが同じメモリ領域を共有します。共用体全体のサイズは、その中で最も大きいサイズのメンバーに合わせて確保されます。
言葉だけでは少し分かりにくいかもしれませんね。まずは、共用体をどのように定義し、使うのかを見ていきましょう。
共用体の定義方法
共用体は `union` キーワードを使って定義します。構造体の `struct` と同じような形式です。
上記の例では、`int` 型の `i`、`float` 型の `f`、そして `char` 型の配列 `str` という3つのメンバーを持つ `Data` という名前の共用体を定義しています。
メンバーへのアクセス
共用体のメンバーへのアクセスは、構造体と同じくドット演算子(`.`)を使います。ポインタの場合はアロー演算子(`->`)です。
このコードを実行すると、各メンバーに値を代入し、それを表示できているように見えます。しかし、最後の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` 演算子を使って共用体と各メンバーのサイズを確認し、メモリ共有の挙動を見てみましょう。
このコードを実行すると、共用体のサイズが最も大きいメンバー(`str[20]`)のサイズ(またはアライメント調整後のサイズ)になっていることがわかります。そして、一方のメンバーに値を代入すると、同じメモリ領域を共有している他のメンバーの「見かけ上の」値が変わってしまう様子が観察できます。これは、同じメモリ上のビットパターンを、異なるデータ型として解釈しようとするためです。
3. 構造体(struct)との比較
共用体と構造体の最も重要な違いはメモリの扱いです。
構造体(struct)
- 各メンバーは独立したメモリ領域を持つ。
- 全体のサイズは、基本的に全メンバーのサイズの合計(+アライメントによるパディング)。
- すべてのメンバーの値を同時に保持できる。
- 例:複数の関連データをひとまとめにする(学生情報:学籍番号、名前、年齢など)。
共用体(union)
- 全メンバーが同じメモリ領域を共有する。
- 全体のサイズは、最も大きいメンバーのサイズ(+アライメント調整)。
- 有効な値を持つメンバーは常に1つだけ(最後に代入されたもの)。
- 例:同じメモリ領域を状況に応じて異なる型として扱いたい場合。
サイズの違いを確認するコード例
このコードを実行すれば、構造体と共用体でメモリサイズが大きく異なることが明確にわかります。構造体はメンバーの合計(+α)、共用体は最大メンバーのサイズになります。
4. 共用体の使いどころ
共用体の「メモリ共有」という特性は、特定の状況で役立ちます。
メモリ使用量の節約
複数のデータ型のうち、同時に1つの型しか使わないことが分かっている場合、共用体を使うことでメモリ使用量を節約できます。例えば、ある変数に整数が入ることもあれば、浮動小数点数が入ることもあるが、両方が同時に入ることはない、といった状況です。
特に、メモリ容量が非常に限られている組み込みシステムなどでは、このメモリ節約効果が重要になることがあります。
ただし、現代の一般的なPCやサーバー環境ではメモリ量が豊富にあるため、単純なメモリ節約目的で共用体を使うメリットは限定的かもしれません。コードの可読性や安全性を考慮すると、構造体を使う方が適切な場合も多いです。
異なるデータ型としてのメモリ解釈(注意が必要)
共用体を使うと、あるデータ型のビットパターンを、別のデータ型として解釈できます。これは、低レベルなプログラミングや、特定のハードウェアレジスタを操作する際などに使われることがありますが、非常に注意が必要な使い方です。
例えば、浮動小数点数の内部表現(ビット列)を整数として調べたい場合などに使われることが考えられますが、これは型の内部表現(エンディアン※など)に強く依存するため、環境が変わると動作しなくなる(移植性が低い)コードになりがちです。
※ エンディアン:複数バイトで構成されるデータをメモリ上に格納する際のバイト順序(ビッグエンディアン、リトルエンディアン)。
また、先述の通り、最後に書き込んだメンバー以外のメンバーを読み出す行為は、多くの場合で未定義動作となるリスクがあります。安全な型変換には、通常のキャスト演算子などを用いるべきです。この目的での共用体の使用は、C言語のメモリ表現に関する深い理解がない限り避けるべきでしょう。
タグ付き共用体(Tagged Union / Variant)
共用体の「どのメンバーが現在有効か」を管理するために、構造体と組み合わせて使う「タグ付き共用体」というパターンがあります。これは、共用体自体と、現在どのメンバーが有効かを示す「タグ」情報(通常は整数型や列挙型)を保持するメンバーを、一つの構造体にまとめたものです。
この方法を使えば、「現在どのメンバーが有効か」という情報(`type` メンバー)に基づいて安全に共用体のメンバーにアクセスできます。共用体の弱点である「どのメンバーが有効かわからない」問題を克服する方法の一つです。
5. 共用体を使う上での注意点
- 有効なメンバーは1つだけ: 共用体に値を書き込むと、それ以前に書き込まれていた他のメンバーの値は無効になります(メモリが上書きされるため)。最後に書き込んだメンバー以外を読み出すことは、基本的に避けるべきです(未定義動作のリスク)。
- 型システムによる保護がない: コンパイラは、あなたが正しいメンバーにアクセスしているかを通常チェックしません。タグ付き共用体のような仕組みを自分で実装しない限り、間違ったメンバーにアクセスしてしまう可能性があります。
- 移植性の問題: メモリの内部表現に依存するような使い方(例:ビットパターンを異なる型で解釈する)をすると、CPUアーキテクチャやコンパイラが異なると期待通りに動作しない可能性があります。
- デバッグの難しさ: ある時点での共用体の値が、どのメンバーとして有効なのかを追跡するのが難しい場合があります。
6. まとめ
- 共用体(union)は、複数のメンバーが同じメモリ領域を共有するデータ構造です。
- 共用体のサイズは、最も大きいメンバーのサイズによって決まります。
- 構造体(struct)は各メンバーが独立したメモリを持つのに対し、共用体は共有します。
- 主な用途は、メモリ使用量の節約(同時に1つの型しか使わない場合)や、タグ付き共用体による多様なデータ表現です。
- 最後に書き込んだメンバー以外へのアクセスは基本的に避けるべきであり、使い方には注意が必要です。
共用体は、構造体ほど頻繁に使われるものではありませんが、C言語のメモリ管理の仕組みを理解する上で非常に興味深い機能です。メモリをどのように効率的に、あるいは柔軟に使うかという視点を与えてくれます。
この知識を活かして、次のステップ「列挙型(enum)と状態管理」に進みましょう!