構造体は「これらすべてを、一緒に」と言います。union(共用体)は「これらのうちちょうど1つを、そのつど」と言います。メンバは同じアドレスに互いの上へ重ねられるので、共用体は最大のメンバと同じ大きさしかなく、1つのメンバへ書き込むと他が壊れます。
罠のように聞こえますし、無防備に使えばそのとおりです。しかしこれは、いくつかの種類のうちのどれかでありうる値 - 数か文字列であるトークン、クリックかキー押下であるメッセージ - を、すべてぶんの代償を払わずにCで表現する方法でもあります。
共用体の宣言と使用
構文は構造体をそのまま映したもので、変わるのはキーワードだけです。
メンバは構造体とまったく同じく .(ポインタ経由なら ->)でアクセスします。違うのは、最も最近書き込まれたメンバだけが意味のある値を保持していることです。v.f = 3.5f のあとに v.i を読んでも 3 にはなりません - 3.5f のビットパターンがたまたま綴る整数が得られます。
サイズ:最大のメンバが勝つ
同じメンバを持つ構造体と共用体のメモリ配置を比べてみましょう。
典型的なマシンでは構造体は24バイト(int に4、double に8、char に1、加えてパディング)で、共用体は8バイト - double の大きさを整列のため切り上げたもの - です。表示される3つのアドレスは同一であり、それが共用体という話のすべてを出力1行で語っています。
%p のための void * へのキャストに注目してください。printf は %p にまさにそれを期待しており、別のポインタ型を渡すのは、たいてい動いているように見えても未定義動作です。書式指定子を参照してください。
共用体を初期化する
指示子のない波かっこの初期化子は最初のメンバを初期化します。
使うべきは指示付きの形です。{42} は静かにメンバの順序に依存するので、あとで宣言を並べ替えるとどのメンバが初期化されるかが変わります - 本当に厄介なバグです。コードの見た目は何も変わらないのですから。
本当の問題:どのメンバが生きているのか
共用体は、最後にどのメンバに書き込んだかを記録しません。ただのバイト列であり、その知識はあなたの頭の中にあります。そしてまさにそこで知識は失われるのです。
表示される数は大きくて奇妙です - 1.0f のビットパターンを int として読んだものです。何もクラッシュせず、何の警告もなく、プログラムは静かに間違っています。共用体は約束したとおりのことをしただけで、間違えたのはどのメンバが生きているかを忘れた私たちです。
対策:タグ付き共用体
標準的な解決策は、生きているメンバを記録する列挙型と共用体を組にし、両方を構造体で包むことです。その組み合わせはタグ付き共用体(または判別共用体)と呼ばれ、アプリケーションのコードではほぼすべての共用体をこう書くべきです。
これでどの読み取りも kind に対する switch を通るので、書き込まれたことのないメンバを読むことは不可能になります - すべての書き込みがタグも設定するかぎりは。書き込みを小さなコンストラクタ関数(value_from_int、value_from_string)で包むのが、それを忘れられなくする通常のやり方です。
メモリの節約は本物です。ここでの Value はペイロード24バイトにタグを加えた大きさで済み、3つすべてを保持する構造体の 4 + 4 + 24 ではありません。それが10万個あれば、これは効いてきます。
-Wall を付けてコンパイルすると安全網がもう1枚増えます。あとで列挙型に VAL_BOOL を追加してその case を忘れたら、GCCが未処理の列挙値について警告してくれます。
無名共用体
C11 は構造体の中に名前のない共用体メンバを許し、そのメンバを外側の構造体の名前空間へ昇格させます。
共用体そのものに名前がないので、s->as.circle.r ではなく s->circle.r と書きます。読むのが短くなる代わりに、そもそも共用体が関わっているという事実が隠れます - タグがすぐ隣にあるならそれで構いません。
共用体が本当に役立つ場面
繰り返し現れる4つの用途です。
- 多様な値。 インタプリタ、JSONや設定のパーサ、メッセージキューはどれも、型が実行時に決まる値を運びます。タグ付き共用体がその正統な表現です。
- メモリの切り詰まったレコード。 構造体がいくつかの排他的なフィールドを持ち、それが何百万個もあるなら、重ね合わせは直接の節約になります。
- プロトコルとハードウェアの配置。 ペイロードがヘッダーのバイトで決まるパケットは自然にタグ付き共用体へ写りますし、デバイスのレジスタも同様です。
- バイトの観察。 値に
unsigned char[]の配列を重ねれば、個々のバイトを見られます。たとえばエンディアンを調べるのに使えます。
これが型パニング、すなわち意図的にバイトを別の型として読み戻すことです。このように共用体を通じて読むことはCでは処理系定義の結果として明示的に許されています(無関係な型の間でポインタをキャストするのとは違い、これはエイリアシングの規則を破りません)し、unsigned char としてバイトを調べることは常に安全です。int を float として再解釈するのは別の話です。結果は完全にプラットフォームの表現に依存するので、可搬なコードからは締め出しておきましょう。
よくある間違い
- 書き込んでいないメンバを読む。 最大の危険です。タグを使いましょう。
- 共用体が変換してくれると思い込む。 しません。
u.i = 3; float f = u.f;はビットを再解釈し、int i = 3; float f = i;は変換します。型キャストを参照してください。 - 共用体にポインタを入れて見失う。 ある分岐が自分で確保した
char *を保持しているとき、別のメンバで共用体を上書きするとそれがリークします - そのバッファを指すものが何も残らないのです。分岐を切り替える前に解放しましょう。 - コンパイラが検査してくれると期待する。 してくれません。共用体は、サイズと整列を超えて言語がまったく助けてくれない数少ないC言語の機能の1つです。タグだけが唯一のガードレールです。
よくある質問
C言語の共用体とは何ですか?
共用体はすべてのメンバが同じメモリを共有する型です。1つのメンバに書き込むと他のメンバが上書きされるので、共用体は一度にちょうど1つのメンバを保持します。構造体と同じように宣言しますが、union キーワードを使います:union Value { int i; float f; };。
C言語の共用体と構造体の違いは何ですか?
構造体はすべてのメンバに自分の記憶領域を与えて全部を同時に保持するので、そのサイズは少なくともメンバの合計になります。共用体はすべてのメンバを同じアドレスに重ね、一度に1つを保持するので、そのサイズは最大のメンバの大きさです。
C言語の共用体のサイズはどうなりますか?
最大のメンバが収まるだけの大きさで、整列のため切り上げられます。int(4バイト)と double(8バイト)の共用体は 12 ではなく 8 バイトです。自分のプラットフォームで確かめるには sizeof を使いましょう。
書き込んでいない共用体のメンバを読むとどうなりますか?
同じバイト列を別の型として再解釈することになります。u.i = 1 と書いてから u.f を読んでも変換はされません - 整数のビットパターンを float として読むので、意味のない数になります。標準Cではそれはよくても未規定であり、だからこそタグ付き共用体のパターンが存在します。どのメンバが生きているかを示すタグを傍らに持たせるのです。