C言語の共用体(union)の使い方、気になっていませんか?
C言語を学んでいると、構造体(struct)はよく使うけれど、共用体(union)って一体何者? どうやって使うの? と疑問符が頭に浮かぶかもしれませんね。見た目は構造体と似ているのに、なんだかちょっと影が薄い存在…? いえいえ、そんなことはありません!
共用体は、メモリを上手に節約したい時や、一つの変数領域で色々な種類のデータを扱いたい時に、とっても便利な仕組みなんです。
この記事では、共用体のそんな「?」を解消します!
- 共用体ってそもそも何? 構造体との違いは?
- どうやってコードに書くの? (宣言方法)
- 宣言した共用体のデータはどうやって使うの? (アクセス方法)
- 実際のプログラムでの使い方をサンプルコードで紹介!
- これだけは押さえて! 共用体を使う時の注意点
この記事を読み終えるころには、共用体の基本がしっかり身についているはず。さあ、一緒に共用体の世界を探検してみましょう! きっとC言語の新しい面白さが見えてきますよ!
C言語の共用体(union)とは?メモリ共有の仕組みを理解しよう
まず、共用体の一番大事な特徴からお話ししましょう。それは、複数のメンバー変数が、たった一つの同じメモリ領域を共有して使うという点です。
構造体(struct)でも解説しましたが、宣言したメンバー変数それぞれに、個別の専用メモリ領域が割り当てられました。例えば、`int`型のメンバーと`double`型のメンバーがいれば、それぞれ別の場所にデータが格納されました。
一方、共用体は違います。メンバー変数がいくつあっても、使うメモリ領域は一つだけ。まるで、一つの部屋を時間帯によってお兄さん(`int`型)が使ったり、お姉さん(`double`型)が使ったりするイメージです。部屋の大きさは、一番大きな家具(一番サイズの大きいメンバー)が入るように決まります。
なぜこんな仕組みがあるのでしょう? 主な理由はメモリの節約です。同時に使うことがない複数のデータを、同じ場所で使いまわせば、メモリの無駄遣いを減らせますよね。また、一つのメモリ領域を異なるデータ型として解釈したい、という少し特殊な場面でも使われたりします。
図でイメージしてみましょう。
構造体 (struct) のメモリイメージ: +-------------------+-------------------------+ | int a; (4バイト) | double b; (8バイト) | +-------------------+-------------------------+ 別々の領域を確保 共用体 (union) のメモリイメージ: +------------------------------------------+ | int i; (4バイト) | | double d; (8バイト) | ここを共有! +------------------------------------------+ 一番大きい double のサイズ (8バイト) 分の領域を確保
このメモリ共有という点が、共用体を理解する上での出発点になります。
C言語における共用体の基本的な書き方(宣言方法)
では、実際に共用体をどうやってコードに書くのか見ていきましょう。書き方は構造体ととてもよく似ています。`struct`の代わりに`union`というキーワードを使います。
基本的な形はこうです。
union 共用体タグ名 { データ型1 メンバー変数名1; データ型2 メンバー変数名2; // ... 必要なだけメンバーを宣言 };
具体例を見てみましょう。整数、浮動小数点数、文字列を格納できる共用体を宣言してみます。
#include <stdio.h> // Dataという名前の共用体を宣言 union Data { int i; float f; char str[20]; // 文字列用に文字配列 }; int main() { // union Data 型の変数 data1 を宣言 union Data data1; // 今はまだ何も解説しませんが、このように変数宣言ができます data1.i = 10; // とりあえず整数を入れてみる printf("data1.i = %d\n", data1.i); return 0; }
`union`キーワードを使って宣言を開始し、続けたい名前(共用体タグ名、ここでは`Data`)を指定します。波括弧`{}`の中に、構造体と同じようにメンバー変数を宣言していきます。最後にセミコロン`;`を忘れないようにしましょう。
毎回`union Data`と書くのが少し面倒だな、と感じるかもしれません。そういう時は、構造体と同じように`typedef`を使うと便利です。
#include <stdio.h> // typedef を使って共用体型に別名 (MyData) をつける typedef union { int i; float f; char str[20]; } MyData; // ← ここで別名を定義 int main() { // MyData 型として変数を宣言できる MyData data2; data2.f = 3.14f; // 浮動小数点数を入れてみる printf("data2.f = %f\n", data2.f); return 0; }
`typedef`を使うと、型名がスッキリしてコードが読みやすくなるというメリットがあります。
C言語での共用体の使い方:メンバーへのアクセス
共用体を宣言したら、次は実際にそのメンバーにデータを入れたり、読み出したりする方法です。これも構造体とほとんど同じ! ドット演算子`.`を使います。
もし、共用体へのポインタ変数を使っている場合は、アロー演算子`->`を使います。
ここで、共用体の最も肝心なルールが登場します。それは、最後に値を代入したメンバーだけが、有効な値(意味のある値)を保持しているということです。一つのメモリ領域を共有しているので、新しいメンバーに値を書き込むと、前に書き込まれていた他のメンバーの値は上書きされて消えてしまう(もしくは意味不明な状態になる)のです!
コードで確認してみましょう。
#include <stdio.h> typedef union { int i; float f; char str[20]; } Data; int main() { Data data; // 1. 整数メンバー i に値を代入 data.i = 99; printf("data.i に代入後: data.i = %d\n", data.i); // この時点では data.f や data.str の値は不定(意味がない) // 2. 浮動小数点数メンバー f に値を代入 data.f = 123.45f; printf("data.f に代入後: data.f = %f\n", data.f); // 3. f に代入した後で i の値を見てみると…? printf("data.f に代入後: data.i = %d\n", data.i); // ← 意味不明な値になるはず! // 4. 文字列メンバー str に値を代入 (文字列は strcpy を使う) strcpy(data.str, "Hello"); printf("data.str に代入後: data.str = %s\n", data.str); // 5. str に代入した後で f の値を見てみると…? printf("data.str に代入後: data.f = %f\n", data.f); // ← これも意味不明な値になるはず! return 0; }
実行してみると、メンバー`f`に代入した後にメンバー`i`の値を見たり、メンバー`str`に代入した後にメンバー`f`の値を見たりすると、元の値とは全く違う、変な値(ゴミデータ)が表示されるはずです(処理系によって結果は異なります)。
共用体を使うときは、今どのメンバーに有効な値が入っているかを、プログラマ自身がきちんと把握しておく必要があります。これが共用体を使いこなす上での最大のポイントと言えるでしょう。
【実践】C言語での共用体の使い方
理屈は分かってきたけど、じゃあ実際にどんな風に使うの?と思いますよね。ここでは、もう少し実践的なサンプルコードを2つ紹介します。見ているだけじゃなく、ぜひお手元の環境で動かしてみてください!動くコードを見るのが理解への一番の近道ですからね。
紹介するコードは、そのままコピー&ペーストしてコンパイル・実行できるように、`#include`や`main`関数も全部含めて書いてあります。
サンプル1:データ型を示すタグと組み合わせる使い方
共用体の一番よくある使い方かもしれません。共用体は「今、どのメンバーが有効か」を自分自身では覚えていてくれません。そこで、どの型のデータが入っているかを覚えておくための目印(タグ)となる別の変数と一緒に使う方法です。
ここでは、整数、浮動小数点数、文字列のどれか一つを格納できるデータ型を作ってみます。どの種類のデータが入っているかは、`enum`(列挙型)を使って管理しましょう。
#include <stdio.h> #include <string.h> // strcpy用 // データの種類を示すための enum (タグ) typedef enum { TYPE_INT, TYPE_FLOAT, TYPE_STRING } DataType; // 共用体本体 typedef union { int i; float f; char str[50]; // 文字列用に少し多めに確保 } Value; // タグと共用体をまとめた構造体 typedef struct { DataType type; // データの種類(タグ) Value value; // データ本体(共用体) } Variant; // Variant 型のデータを表示する関数 void printVariant(Variant data) { printf("データ型: "); switch (data.type) { case TYPE_INT: printf("整数, 値: %d\n", data.value.i); break; case TYPE_FLOAT: printf("浮動小数点数, 値: %f\n", data.value.f); break; case TYPE_STRING: printf("文字列, 値: %s\n", data.value.str); break; default: printf("不明なデータ型\n"); break; } } int main() { Variant var1, var2, var3; // 整数データを設定 var1.type = TYPE_INT; var1.value.i = 100; // 浮動小数点数データを設定 var2.type = TYPE_FLOAT; var2.value.f = 98.76f; // 文字列データを設定 var3.type = TYPE_STRING; strcpy(var3.value.str, "C Language"); // 文字列代入 // それぞれ表示してみる printVariant(var1); printVariant(var2); printVariant(var3); return 0; }
ソースコードの表示結果(例):
データ型: 整数, 値: 100 データ型: 浮動小数点数, 値: 98.760002 データ型: 文字列, 値: C Language
解説:
まず、`DataType`という`enum`でデータの種類(整数、浮動小数点数、文字列)を定義しています。
次に、`Value`という共用体で実際の値を格納する場所を定義。そして、`Variant`という構造体で、`DataType`型のタグ`type`と、`Value`型の共用体`value`を一つにまとめています。
`main`関数では、`Variant`型の変数を3つ用意し、それぞれに異なる種類のデータを設定しています。データを設定する際は、まず`type`メンバーにデータの種類(`TYPE_INT`など)を設定し、それから`value`メンバーの適切なメンバー(`value.i`など)に値を代入します。
`printVariant`関数では、引数で受け取った`Variant`データの`type`メンバーを見て、`switch`文でどの型のデータかを判断し、共用体の適切なメンバーにアクセスして値を表示しています。
このようにタグと組み合わせることで、「今どのメンバーが有効か」をプログラムで管理できるようになり、共用体を安全に使えるようになります。
サンプル2:メモリサイズを意識したデータ構造
共用体のメリットの一つである「メモリ節約」を実感してみましょう。ここでは、共用体を使った場合と、使わずに全ての可能性のあるメンバーを構造体に入れた場合で、どれくらいサイズが変わるかを見てみます。
仮に、あるデータが、場合によっては「ID番号(long型、8バイト)」として使われ、また別の場合によっては「短い名前(char型配列、10バイト)」として使われる、という状況を考えてみましょう。
#include <stdio.h> #include <stddef.h> // offsetof や size_t 用 (主に sizeof のため) // 共用体を使ったデータ構造 typedef union { long id; // 8バイト (仮定) char name[10]; // 10バイト } DataUnion; // 比較用:共用体を使わずに両方のメンバーを持つ構造体 typedef struct { long id; // 8バイト (仮定) char name[10]; // 10バイト } DataStruct; int main() { printf("共用体 DataUnion のサイズ: %zu バイト\n", sizeof(DataUnion)); printf("構造体 DataStruct のサイズ: %zu バイト\n", sizeof(DataStruct)); return 0; }
ソースコードの表示結果(例):
※実行環境(OS、コンパイラ、CPUアーキテクチャ)によってサイズは変わることがあります。
共用体 DataUnion のサイズ: 16 バイト 構造体 DataStruct のサイズ: 24 バイト
解説:
`DataUnion`は共用体なので、そのサイズはメンバーの中で一番大きい`name[10]`(10バイト)に合わせて…と思いきや、多くの環境ではメモリのアライメント(整列)というパディング(隙間埋め)が行われるため、キリの良いサイズ(例えば8の倍数である16バイトなど)になることがあります。それでも、共用体のサイズは基本的に「最も大きいメンバー」が基準になります。
一方、`DataStruct`は構造体なので、メンバー`id`(8バイト)と`name[10]`(10バイト)の両方を格納するための領域が必要です。これもアライメントの影響で、単純な合計(8+10=18バイト)ではなく、パディングが挿入されてより大きなサイズ(例えば24バイトなど)になることがあります。
結果を見ると、共用体を使った方がメモリサイズが小さくなっていますね! この例では劇的な差ではありませんが、扱うデータがもっと大きかったり、たくさんのデータ配列を扱ったりする場合、共用体によるメモリ節約効果は無視できないものになるでしょう。
C言語の共用体を使う上での重要な注意点
共用体は便利ですが、その特性を理解していないと思わぬ落とし穴にはまることも。ここでは、「これだけは絶対に覚えておいて!」という注意点を3つ紹介します。ルールを知って、安全に共用体を使いましょう!
【注意点1】有効なメンバーは常に最後に代入したものだけ
しつこいようですが、これが一番の基本ルールです! 共用体は一つのメモリ領域を使い回しているので、あるメンバーに値を書き込むと、他のメンバーが以前に保持していた値は上書きされて意味をなさなくなります。
メモリ領域 (例: 4バイト) +-----------------+ | | +-----------------+ data.i = 10; を実行すると… +-----------------+ | 10 (整数) | +-----------------+ 次に data.f = 3.14f; を実行すると… (同じ領域に上書き!) +-----------------+ | 3.14f (浮動小数)| +-----------------+ ↑ 以前の 10 という整数データはもうどこにもありません!
サンプル1で見たように、タグ変数などを使って「今どのメンバーが有効か」を自分で管理する仕組みを作るのが、安全に使うための定石です。
【注意点2】異なる型のメンバーとしてアクセスするのは危険
例えば、`int`型のメンバーに値を代入した後、そのメモリ領域を`float`型のメンバーとして読み出すとどうなるでしょう? これは非常に危険な操作です。コンピューターはメモリ上のビット列(0と1の並び)を、「これは整数だ」「これは浮動小数点数だ」と解釈して値を扱います。同じビット列でも、整数として解釈する場合と浮動小数点数として解釈する場合では、全く違う値になってしまいます。
#include <stdio.h> typedef union { int i; float f; } Number; int main() { Number num; num.i = 1078523331; // ある整数値 (例) // 同じメモリ領域を float として解釈して表示 printf("整数として代入した値を浮動小数点数として見ると: %f\n", num.f); return 0; }
実行すると、おそらく`1078523331`とは似ても似つかない浮動小数点数値が表示されるはずです(例えば `3.140000` 近くになるかも?値は処理系依存です)。
意図的に型変換を行いたい特殊なケースを除き、最後に代入したメンバーの型と違う型のメンバーとしてアクセスするのは避けるべきです。予期せぬバグの温床になります。
【注意点3】初期化は基本的に最初のメンバーに対してのみ
共用体変数を宣言するときに、同時に初期値を設定することができます。ただし、標準的なC言語の書き方では、波括弧 `{}` を使って初期化する場合、その値は共用体の「最初に宣言されたメンバー」に対して設定されるというルールがあります。
#include <stdio.h> typedef union { int count; // 最初に宣言されたメンバー double value; } Measure; int main() { // {10} は最初のメンバー count に対する初期化となる Measure m1 = {10}; printf("m1.count = %d\n", m1.count); // printf("m1.value = %f\n", m1.value); // ← この時点での value の値は不定 // C99以降の指示付き初期化子を使えば、他のメンバーも初期化できる(おまけ知識) // Measure m2 = {.value = 9.8}; // printf("m2.value = %f\n", m2.value); return 0; }
上記の例では、`{10}` という初期値は、最初に宣言されている`int count`メンバーに設定されます。
`double value`メンバーを直接初期化したい場合は、コメントアウトしているような「指示付き初期化子」(C99以降の機能)を使う方法もありますが、まずは「最初のメンバーが初期化される」という基本ルールを覚えておきましょう。
C言語の共用体と構造体の違い・使い分けまとめ
さて、ここまで共用体について見てきましたが、もう一度、よく似た構造体との違いを整理しておきましょう。どちらを使うべきか迷ったときの判断材料にしてください。
- メモリの割り当て
- 共用体(union): 全メンバーで一つのメモリ領域を共有する。
- 構造体(struct): 各メンバーがそれぞれ固有のメモリ領域を持つ。
- 全体のサイズ
- 共用体(union): メンバーの中で最も大きいデータ型のサイズが基準となる(+アライメント)。
- 構造体(struct): 全メンバーのサイズの合計が基準となる(+アライメント)。
- メンバーへの同時アクセス
- 共用体(union): 同時に有効な値を持つメンバーは一つだけ。
- 構造体(struct): 全メンバーが常に有効で、同時にアクセスできる。
- 主な用途
- 共用体(union): メモリを節約したい場合。一つの変数で状況に応じて異なる型のデータを保持したい場合(タグと組み合わせることが多い)。
- 構造体(struct): 関連する複数のデータをひとまとめにして扱いたい場合(例:座標(x, y)、個人情報(名前, 年齢)など)。
簡単に言うと、
- 複数のデータを同時に保持したいなら → 構造体
- 複数のデータのどれか一つを状況に応じて保持し、メモリを節約したいなら → 共用体
という使い分けが基本になります。
【まとめ】C言語の共用体を理解して適切に活用しよう
お疲れさまでした! C言語の共用体(union)について、基本的な概念から書き方、使い方、注意点、そして構造体との違いまで、一通り見てきました。
共用体の肝は、なんといっても「メモリの共有」でしたね。そのおかげでメモリを節約できる可能性がある一方、「有効なメンバーは最後に代入したものだけ」という大事なルールがありました。
最初は少しとっつきにくいかもしれませんが、仕組みを理解してしまえば、共用体はあなたのC言語プログラミングの引き出しを増やしてくれるはずです。特に、組み込み系などメモリが限られた環境では、共用体の知識が役立つ場面もあるでしょう。
この記事で学んだことを元に、ぜひご自身のコードで共用体を試してみてください。実際に手を動かしてみるのが、一番の力になりますからね!
【関連記事】
0 件のコメント:
コメントを投稿
注: コメントを投稿できるのは、このブログのメンバーだけです。