このページでは、C言語の共用体(union
)について説明していきます。
共用体は、使い方に関しては構造体と似ていますが、全く異なる特徴を持つデータの構造になります。これらの違いを理解することが、共用体の特徴の理解につながります。
そのため、共用体を理解するためには構造体の知識が必須となります。他にもメモリやアドレスなどの知識も必要で、C言語の基本を一通り理解していないと共用体の特徴を理解することは難しいと思います。なので、実は共用体の使い方を理解していても、特徴やメリットを理解していない人は多いのではないかと思います。
このページでは、共用体の特徴が理解しやすいように、分かりやすく共用体についてまとめていますので、是非このページを読んで共用体の理解に挑戦してみてください!
また、特に構造体に関しては下記ページで解説していますので、構造体をご存知ない方は事前に下記ページを読んでおくことをオススメします。
【C言語】構造体について初心者向けに分かりやすく解説Contents
共用体とは
では、さっそく共用体について説明していきます。
共用体は「同じ1つのデータを複数のメンバーで共用して管理するデータの構造」のことを言います。この一文は共用体の特徴全てを表していると言っても過言ではないです。ただ、この一文だけだと特徴がイメージしにくいと思いますし、この特徴のメリットを理解できない方も多いと思いますので、この共用体の特徴に関しては 共用体と構造体の違い 以降で詳細を説明していきたいと思います。
C言語では、共用体の型を自身で定義し、その型の変数を宣言して利用することが可能です。例えば、int
や float
など、プログラミング言語の仕様として予め用意されている型と同様に変数宣言して利用することが可能です。ただし、前述のとおり、共用体の型に関しては、自身で定義してから使用する必要があります。
共用体の使い方
続いて共用体の使い方について解説していきます。
スポンサーリンク
型の定義
共用体はデータの構造であり、その構造を持つ変数を利用するためには型を定義しておく必要があります。
共用体の型は下記のような形式で定義することができます。union
が「この型を共用体として定義する」ことを指示する識別子となります。そして、下記のように定義を行うことで、以降、union タグ名
が新たに定義した共用体の型の「型名」として扱われるようになります。
union タグ名 {
型 メンバー名1;
型 メンバー名2;
// 略
};
そして、この型のデータは、 {
~ }
の内側で指定した型のメンバーを持つことになります。メンバーとは、この定義した型が持つ要素のことです。上記では2つのメンバーを持たせていますが、もっと多くのメンバーを持たせることも可能です。
また、型
には既に定義済みの型であれば基本的に何でも指定することが可能です。int
や float
はもちろん、事前に定義した構造体の型を指定しても良いです。
例えば、下記は union _Number
という型を共用体として定義する例となります。
union _Number {
short short_num;
int int_num;
float float_num;
double double_num;
};
変数宣言
型として定義してやれば、後は通常の型の時と同様の使い方で共用体のデータを利用することが出来るようになります。例えば int
型の変数は変数宣言を行ってから、その宣言した変数をソースコード上で利用することになります。共用体の型のデータも同様の手順で利用していくことになります。
より具体的には、下記のように union タグ名 変数名;
を記述することで共用体の型の変数を宣言することが可能です。そして、この宣言した変数はプログラム内で利用することが可能となります。
union タグ名 変数名;
例えば、前述で示した例では union _Number
を定義していますので、下記のような記述を行うことで変数 number
を利用することが出来るようになります。
union _Number number;
もちろん、定義した型の配列を宣言することも可能です。
union _Number numbers[100];
変数の利用
変数宣言してやれば、後はその変数を利用して実現したい処理をソースコードに記述していけば良いだけになります。ただし、基本的には共用体の型の変数は、変数そのものではなく、その変数の持つメンバーを使って値の代入や値の取得を行うことになります。構造体と同様に、メンバーは 変数名
の後ろに .メンバー名
と記述することで利用することが出来ます。
union タグ名 変数名;
変数名.メンバー名 = 代入したい値;
printf("%d\n", 変数名.メンバー名);
例えば下記は、変数 number
のメンバー int_num
に 100
を代入し、さらに int_num
の値を出力する例となります。
union _Number number;
number.int_num = 100;
printf("%d\n", number.int_num);
スポンサーリンク
アロー演算子も利用可能
また、これも構造体と同様に、共用体の型のデータを指すポインタ変数からメンバーを利用する際には “アロー演算子(->
)” を利用することになります。
例えば下記は、変数 number
を指すポインタ p_number
から number
のメンバー int_num
を利用する例となります。
union _Number number;
union _Number *p_number = &number;
p_number->int_num = 100;
printf("%d\n", number.int_num);
アロー演算子については下記ページで詳しく説明していますので、アロー演算子について知りたい方は是非下記ページを読んでみていただければと思います。
C言語のアロー演算子(->)を分かりやすく、そして深く解説typedef
でシンプルな型名を付けることも可能
また、構造体等の他の型と同様に、typedef
を利用して共用体の型を “新たな名前の型” として定義することも可能です。
共用体の型に対して typedef
によって新たな型を定義する際には、下記のように typedef
の直後に union タグ名
を、その後ろ側に 新たな型名
を指定してやれば良いです。typedef
によって共用体の型に新たに型名を付けることで、わざわざ共用体の型の変数を宣言する際に型名に union
を指定する必要がなくなります。
typedef union タグ名 新たな型名;
例えば、下記のように typedef
してやれば、新たな型 NUMBER
が定義されることになり、この NUMBER
は union _Number
と全く同じ構造のデータの型となります。
union _Number {
short short_num;
int int_num;
float float_num;
double double_num;
};
typedef union _Number NUMBER;
さらに、下記のように共用体の型を定義する際に typedef
も同時に行うことで、共用体の定義と同時に新たな型を定義することが可能となります。この場合は タグ名
も省略することが可能です。
typedef union {
short short_num;
int int_num;
float float_num;
double double_num;
} NUMBER;
共用体と構造体の違い
さて、ここまで共用体の使い方について説明してきました。ここまでの説明で理解していただけたと思いますが、共用体の型の変数の使い方は構造体の型の変数の使い方と同様になります。
使い方としては同じなのですが、この2つにはデータの構造に決定的な違いがあります。そして、その違いが共用体の特徴となります。ということで、次は共用体と構造体の違いについて説明していきたいと思います。
スポンサーリンク
各メンバーが同じメモリを共用する
共用体と構造体の決定的な違いは、各メンバーのメモリ上の配置位置となります。
まず、構造体では各メンバーが必ずメモリ上の異なるアドレスに配置されることになります。そして、各メンバーのデータが重なり合うことはなく、それぞれのメンバーが独立して存在することになります。つまり、各メンバーの変更は、他のメンバーの変更に影響を及ぼすことはありません。
それに対し、共用体では、各メンバーがメモリ上の同じアドレスに配置されることになります。より正確に言えば、各メンバーの先頭アドレスが同じ位置となります。そのため、各メンバーが同じメモリ領域を共用することになります。そのため、各メンバーの変更は他のメンバーの変更に影響を及ぼすことになります。
例えば、下記のようなソースコードについて考えてみましょう。ここでは構造体の型 S_DATA
と共用体の型 U_DATA
の2つを定義し、それぞれの変数を s_data
と u_data
として宣言しています。S_DATA
と U_DATA
のメンバーは同じとしており、各メンバーの先頭アドレスを printf
関数で出力しています。
#include <stdio.h> // printf
typedef struct {
int num1;
int num2;
int num3;
} S_DATA;
typedef union {
int num1;
int num2;
int num3;
} U_DATA;
int main(void) {
S_DATA s_data;
U_DATA u_data;
printf("s_data.num1 : %p\n", &s_data.num1);
printf("s_data.num2 : %p\n", &s_data.num2);
printf("s_data.num3 : %p\n", &s_data.num3);
printf("u_data.num1 : %p\n", &u_data.num1);
printf("u_data.num2 : %p\n", &u_data.num2);
printf("u_data.num3 : %p\n", &u_data.num3);
}
ここまでの説明を読んでいただいた方であれば予想はつくと思いますが、上記をコンパイルして実行すると次のような出力結果が得られることになります。下記は私の PC で実行した結果であり、皆さんが実行した場合も、出力されるアドレスは異なるものの、s_data
の各メンバーのアドレスがそれぞれ異なることと、u_data
の各メンバーのアドレスがそれぞれ同じであることが確認できると思います。
s_data.num1 : 0x7fffffffde4c s_data.num2 : 0x7fffffffde50 s_data.num3 : 0x7fffffffde54 u_data.num1 : 0x7fffffffde48 u_data.num2 : 0x7fffffffde48 u_data.num3 : 0x7fffffffde48
プログラムにおいては、各変数はメモリ上に自動的に配置されることになります。これは構造体や共用体のメンバーも同様です。構造体の場合は、各メンバーが定義時に指定した順序で上から順に自動的にメモリ上に配置されることになります(1番上に持たせているメンバーは変数の先頭アドレスと一致します)。
これらは独立して別々に扱われるため、これらのメンバーが同じアドレスに配置されることも、重なり合って配置されることもありません。それに対し、共用体の各メンバーは同じアドレスに配置されることになります。そのため、各メンバーは重なり合って配置されることになります。
共用体の変数で管理できるデータは基本的に1種類のみ
共用体の各メンバーは同じアドレスに重なり合って配置されるため、特定のメンバーに対して数値を代入したりデータをコピーすると他のメンバーのデータも変化することになります。
例えば下記のソースコードに注目してみましょう。下記では、構造体と共用体の各メンバーに対して整数の代入を行い、最後に printf
で各メンバーに格納されている整数を出力しています。
#include <stdio.h> // printf
typedef struct {
int num1;
int num2;
int num3;
} S_DATA;
typedef union {
int num1;
int num2;
int num3;
} U_DATA;
int main(void) {
S_DATA s_data;
U_DATA u_data;
s_data.num1 = 1;
s_data.num2 = 2;
s_data.num3 = 3;
u_data.num1 = 1;
u_data.num2 = 2;
u_data.num3 = 3;
printf("s_data.num1 : %d\n", s_data.num1);
printf("s_data.num2 : %d\n", s_data.num2);
printf("s_data.num3 : %d\n", s_data.num3);
printf("u_data.num1 : %d\n", u_data.num1);
printf("u_data.num2 : %d\n", u_data.num2);
printf("u_data.num3 : %d\n", u_data.num3);
}
上記ソースコードをコンパイルして実行した場合、printf
で出力される結果は下記のようなものになるはずです。
s_data.num1 : 1 s_data.num2 : 2 s_data.num3 : 3 u_data.num1 : 3 u_data.num2 : 3 u_data.num3 : 3
この結果からも分かるように、構造体の各メンバーは独立して別々に存在するため、各メンバーに代入した値は他のメンバーへの代入等に影響されず保持されることになります。そのため、構造体の場合はメンバーの数だけ別々のデータを管理することが出来ることになります。
それに対し、共用体の場合は同じアドレスに各メンバーが存在するため、特定のメンバーへの数値の代入やデータのコピーが行われると、他のメンバーに代入した数値等が上書きされてしまうことになります。その結果、上記のようなソースコードの場合はすべてのメンバーの出力結果が同じ値となっています。
共用体の場合、各メンバーは同じアドレスに配置されているため、これらは同じデータということになります。なので、上記のように特定のメンバーへの数値の代入やデータのコピーが行われると、他のメンバーに代入した数値等が上書きされてしまうことになります。
つまり、共用体では、メンバーがいくつ存在したとしても、基本的に1つの変数で管理できるデータは1つのみであることになります。そして、その1つのデータを複数のメンバーからアクセス(変更や取得)することができるようになっています。
では、1つのデータに複数の異なるメンバーからアクセスできるメリットは何でしょうか?ここが共用体を理解する1つのポイントになると思います。このあたりについては後述で解説していきます。
共用体の型のサイズはメンバーの最大サイズとなる
また、構造体の型のサイズは「全てのメンバーのサイズの和(+データの整列用の調整サイズ)」となります。したがって、メンバーの数が増えれば増えるほど構造体の型のサイズは大きくなることになります。構造体の場合、メンバーの数だけのデータを管理することになりますので、管理するデータが増える分、型のサイズも大きくなることになります。
それに対し、共用体の場合は管理するデータは1つのみであり、共用体の型のサイズは「一番サイズの大きなメンバーのサイズ(+データの整列用の調整サイズ)」となります。
例えば下記のようなソースコードについて考えてみましょう。下記では構造体の型と共用体の型のサイズを printf
で出力しています。
#include <stdio.h> // printf
typedef struct {
char num1;
int num2;
double num3;
} S_DATA;
typedef union {
char num1;
int num2;
double num3;
} U_DATA;
int main(void) {
printf("size of S_DATA : %ld\n", sizeof(S_DATA));
printf("size of U_DATA : %ld\n", sizeof(U_DATA));
}
私の PC で実行した結果は下記となりました。私の PC では char
型のサイズが 1
バイト、int
型のサイズが 4
バイト、double
型のサイズが 8
バイト、さらに構造体や共用体の各メンバが 4
バイト単位で整列されるようになっているので下記のような結果になりましたが、これらが異なる環境で実行した結果は下記とは異なる結果になると思います。
size of S_DATA : 16 size of U_DATA : 8
が、いずれにせよ構造体の型のサイズは「全てのメンバーのサイズの和(+データの整列用の調整サイズ)」となり、共用体の型のサイズは「一番サイズの大きなメンバーのサイズ(+データの整列用の調整サイズ)」として出力されるはずです。
ここで、少しだけ、データの整列用の調整サイズについて説明しておくと、構造体や共用体の特定の型のメンバーは、先頭アドレスが特定のバイトの倍数となるように調整されるようになっています。また、これらのサイズも特定のバイトの倍数となるようになっています。
そのため、構造体のサイズは各メンバーのサイズを単に足し合わせたサイズと一致しない場合がありますし、共用体のサイズは一番大きなメンバーのサイズと一致しない場合があります。
ちょっとここがややこしいので、まずは構造体のサイズは「各メンバーのサイズの和」に基づいて決まり、共用体のサイズは「一番大きなメンバーのサイズ」に基づいて決まる、くらいで覚えておいても良いと思います。要は、構造体の各メンバーは別々の位置に重なり合わないように配置されるため、最低でも各メンバーのサイズの和が必要になります。それに対し、共用体の各メンバーは同じ位置に重なり合うように配置されるため、一番大きなメンバーのサイズさえあれば十分ということになります。
スポンサーリンク
共用体と構造体の違いのまとめ
ここで、ここまで説明してきたことを一旦まとめておきたいと思います。
- 各メンバーが配置される位置
- 共用体:各メンバーが重なって同じアドレスに配置される
- 構造体:各メンバーが重ならないように別々のアドレスに配置される
- 管理できるデータの数
- 共用体:1つのみ
- 構造体:メンバーの数だけ
- サイズ
- 共用体:1番大きなメンバーのサイズ+α
- 構造体:各メンバーのサイズの和+α
こうやって見ると、同じような記述で定義ができ、使い方も同様ではあるものの、共用体と構造体は全く異なるものであることが理解していただけると思います。
構造体の用途は明確で、1つの変数で複数のデータをまとめて関連付けて管理したい時に使用します。例えば「人」には、その人を特徴づけるデータがたくさんあります。例えば名前・年齢・身長などなど。これらを関連付けてまとめて管理することで、複数のデータを管理しやすくし、さらにソースコードも読みやすくなります。
それに対し、共用体はどんな時に利用するのでしょうか?
わざわざ複数のメンバーを持たせているのに、1つのデータしか管理することができないなんて意味不明ですよね…。
なんですが、実は共用体の用途は存在し、共用体も利用するメリットもあります。次は、共用体を利用するメリットについて説明していきたいと思います。
共用体のメリット
では、共用体を利用するメリットについて解説していきます。ここを理解すれば適切に共用体や他の型とを使い分けることができるようになると思います!
異なる型へのデータ変換が容易
まず、共用体のメリットの1つ目として、データを異なる型に簡単に変換できるという点が挙げられます。前述のとおり、共用体の型の1つの変数で管理できるデータは1つのみです。そして、その1つのデータを複数の異なるメンバーから取得することが出来ます。そして、その取得できるデータは、取得を試みたメンバーの型に変換されたデータとなります。
そのため、1つのデータを様々な型や構造のデータに変換することが容易となります。
具体例を見てみましょう!
下記は、共用体の型である U_DATA
を定義し、U_DATA
の変数である u_data
のデータを各メンバーから取得する例になります。
#include <stdio.h> // printf
typedef union {
int int_num;
short short_num;
unsigned char bytes[4];
float float_num;
} U_DATA;
int main(void) {
U_DATA u_data;
u_data.int_num = 1094713344;
// int型で出力
printf("int_num : %d\n", u_data.int_num);
// short形で出力
printf("short_num : %d\n", u_data.short_num);
// 1バイトずつ出力
printf("bytes:0x");
for (int i = 0; i < 4; i++) {
printf("%02x", u_data.bytes[i]);
}
printf("\n");
// float型で出力
printf("float_num : %f\n", u_data.float_num);
}
前述のとおり、u_data
で管理できるデータは1つのみです。そして、u_data
には、u_data.int_num = 1094713344
によって int
型で 1094713344
という整数が格納されていることになります。
上記のソースコードでは、このu_data
を各メンバーから取得して printf
で出力を行っています。そして、この出力結果は下記のようになります。もしかしたら環境によって異なる出力結果になっているかもしれませんが、多くの方が下記のような出力結果を得ることが出来ているのではないかと思います。
int_num : 1094713344 short_num : 0 bytes:0x00004041 float_num : 12.000000
int_num
メンバーの出力結果が上記のようになるのは納得がいくと思います。しかし、他のメンバーの出力結果はどうして上記のようなものになるのでしょうか?
この理由が、共用体のメリットにつながります。ということで、上記のような出力結果となる理由について説明しておきます。
コンピューター内部で管理されるデータ
まず前提として、コンピューター内部で扱われるデータはすべて 1
or 0
となります。この2種類のデータを組み合わせたり、見方や扱い方を変えることで無限の種類のデータをコンピューターで表現することが出来るようになります。そして、この 1
or 0
の数字からのみ表現される数を2進数と呼びます。
例えば、上記では int_num
メンバーに 1094713344
という値を代入していますが、この値を2進数で表現すると下記になります。
01000001010000000000000000000000
コンピューターで実際に扱われるデータは 1
or 0
になるのですが、これだと人間には分かりにくいので、バイト単位でデータを表現することが多いです。コンピューターで実際に扱われるデータ(1
or 0
)のサイズを表す単位はビットであり、このビットを 8
個分まとめたデータのサイズの単位がバイトとなります。そして、1バイトのデータは16進数で表現することが多いです。これは、16進数で表すことで、1バイトが必ず2桁以下の数値となってバイト単位の区切りが人間にとって分かりやすくなるからです。
例えば、1094713344
という整数を16進数で表すと下記のようになります(0x
は、その数値が16進数表記であることを示します)。
0x41400000
つまり、1094713344
を int_num
に代入した場合、16進数で考えると 0x41400000
という値が代入されることになります。そして、この2桁ずつの値が1バイトのサイズとなりますので、0x41
と 0x40
と 0x00
と 0x00
の合計4バイトが udata.int_num
のアドレスに格納されることになります(代入とは、変数の先頭アドレスにバイトデータを格納する処理であると考えられます)。
で、この4バイト分のデータを先頭から順に int_num
の先頭アドレスから並べて格納した場合、udata.int_num
の先頭アドレスから4バイト分のデータは、1094713344
の代入によって下記のように変化することになりそうですね。
0x41 0x40 0x00 0x00
なんですが、このデータの並びはデータの先頭から順ではなく、データの末尾から順に特定のアドレスに格納されることが多いです。このあたりは CPU によって異なり、具体的にはエンディアン(リトルエンディアン or ビッグエンディアン)によって並び順は変わります。私の PC の場合は CPU がリトルエンディアンなので、1094713344
を代入した際には 0x41400000
の末尾側の2桁から順に udata.int_num
のアドレスにデータが格納されることになります。
つまり、int_num
メンバーへの 1094713344
代入後、udata.int_num
の先頭アドレスから4バイトは下の図のようになることになります。つまり、int_num
メンバーの先頭アドレスから順に、0x00
0x00
0x40
0x41
が格納されていることになります。
そして、共用体の場合、各メンバーは同じアドレスに配置されることになります。したがって、short_num
の先頭アドレスから4バイトも、bytes
の先頭アドレスから4バイトも、float_num
の先頭から4バイトも同様に 0x00
0x00
0x40
0x41
が格納されていることになります(下図では各メンバーのデータを別々に記載していますが、これらは同じアドレスのデータとなります)。
ここで重要なのは、ここまでも説明してきたように、メンバーがいくつあろうが共用体の型の変数で管理できるデータは1つのみで、各メンバーの先頭アドレスからは同じデータが格納されているという点になります。
では、この状態で各メンバーの値を出力すると、どういったデータが出力されることになるのか、という点について説明していきます。
int
型のメンバーの出力
まずは int_num
の値の出力について考えていきましょう。int_num
には直接 1094713344
が代入されているので 1094713344
が出力されることは当然ではあるのですが、ここでは int_num
の先頭から4バイトのデータに注目して出力される値を考えていきたいと思います。
前述の通り、int_num
メンバーの先頭アドレスから4バイトには 0x00
0x00
0x40
0x41
が格納されています。さらに、int
型のサイズが4バイトなので、ここで int_num
メンバーの先頭アドレスから4バイトを取得し、さらにエンディアンの関係で逆順にしていた並びを元に戻して4バイト分のデータを結合すると、16進数で 0x41400000
が得られます。そして、これを int
型の10進数の整数に変換すると 1094713344
となり、これが int_num
メンバーの値として出力されることになります。
つまり、メンバーの先頭アドレスから格納されている「メンバーの型のサイズ分のデータ」を取得し、データの並びを元に戻し、さらに「メンバーの型に応じたデータ」に変換することで、出力する値が決まることになります。これは、メンバーが int
型の場合だけでなく、メンバーが他の型の場合でも同様になります。
そして、int_num
メンバーを出力すると 0x00
0x00
0x40
0x41
の4バイトを int
型として扱った場合の値が出力されることになり、その出力結果は 1094713344
となります。
int_num : 1094713344
short
型のメンバーの出力
では、次は short_num
の値の出力について考えていきましょう。
short_num
の型は short
であり、私の PC では short
は2バイトで扱われます。したがって、short_num
の先頭アドレスから2バイト分のみを取得し、データの並びを元に戻し、さらに short
型の10進数の整数に変換した結果が short_num
を出力した時に得られる値となります。今回の場合、short_num
の先頭アドレスから2バイト分のデータは 0x0000
となりますので、並びを元に戻しても 0x0000
であり、これは short
型の10進数で 0
となります。
したがって、short_num
メンバーを出力した場合、0x00
0x00
0x40
0x41
の2バイトを short
型として扱った場合の値が出力されることになり、その出力結果は 0
となります。
short_num : 0
unsigned char
型の配列のメンバーの出力
次は bytes
の場合についても考えてみましょう!
bytes
の場合はループの中で bytes[0]
から bytes[3]
の値の出力を行なっている&16進数で出力しているので少し複雑に思えますが、考え方は今までと同様です。
まず、最初のループでは bytes[0]
の出力が行われます。bytes
は unsigned char
型の配列であり、unsigned char
型のサイズは1バイトです。そして、bytes[0]
は、bytes
の先頭アドレスの1バイトのデータになります。
したがって、bytes[0]
を出力する際には、bytes
の先頭アドレスの1バイトのデータが取得され、データの並びが元に戻され、さらにそのデータが unsigned char
型に変換されることになります(1バイトなのでデータの並びは変化しません)。
さらに、これらの bytes
のデータを出力する際には printf
のフォーマットとして "%02x"
が指定されています。このようにフォーマットを指定した場合は2桁の16進数として値が出力されることになります。上図でも示しているように、bytes[0]
を最終的に unsigned char
型に変換したデータは 0
なので、bytes[0]
の出力結果としては 00
が得られることになります。
bytes[1]
から bytes[3]
に関しても、bytes[1]
から bytes[3]
のアドレスのデータに同様の変換が行われて出力されることになります。
したがって、bytes
メンバーを先頭から1バイトずつ出力した場合、0x00
0x00
0x40
0x41
の各バイトそれぞれが unsigned char
型として扱った場合の値が出力されることになり、その出力結果は 00004041
となります。
bytes:0x00004041
float
型のメンバーの出力
最後の float_num
はちょっとややこしいです。
float_num
の先頭アドレスから float
のサイズ分のデータを取得し、データの並び順を元に戻すところまでは、今までの説明と同様になります。
問題は、この並び順が元に戻された結果の 0x41400000
がどのようにして float
型に変換されるのか、という点になります。
まず、float
は単精度の浮動小数点数を扱う型であり、この単精度の浮動小数点数は下記のような形式の4バイトのデータで扱うことが IEEE754 の規格によって定められています。
そして、この4バイトのデータの各ビットの値から求まる「符号部」・「指数部」・「仮数部」を用いて、下記の式によって計算される値が、その4バイトのデータを浮動小数点数として扱った場合の値となります。
何を言っているか意味不明…と思われた方は、ぜひ下記ページを読んでみてください。浮動小数点数のコンピューター内部でのデータの扱いについて詳しく説明しています。
【C言語】浮動小数点数における「数値⇔内部データ(符号部・指数部・仮数部)」の変換さて、先ほど並び順を元に戻して得られた 0x41400000
を上記の考え方に基づいて float
型のデータに変換してみましょう!まずは、この 0x41400000
を2進数に変換すると、結果は下記のようになります。
01000001010000000000000000000000
さらに、この2進数における 0
と 1
の並びを、先ほど図示した IEEE754 の規格で定められたデータの形式に当てはめると、符号部、指数部、仮数部はそれぞれ下記のようになります。
- 符号部:
0
- 指数部:
10000010
- 仮数部:
10000000000000000000000
これらの符号部・指数部・仮数部のそれぞれを10進数に変換し、さらに前述で示した計算式に当てはめると、式は次のようになります。そして、この計算結果は 12.0
となります。
したがって、float_num
メンバーを出力した場合、0x00
0x00
0x40
0x41
の4バイトを float
型として扱った場合の値が出力されることになり、その出力結果は 12.0
となります(小数点以下の桁数はもっと多くなります)。
float_num : 12.000000
メンバーの型に応じてデータの扱い方が変わる
長々と説明してきましたが、ここまでの例での最大のポイントは、0x00
0x00
0x40
0x41
の4バイトのデータが、利用するメンバーに応じて変化するという点になります。
ここまで説明してきたように、変数や共用体のメンバー等に値を代入した場合、コンピューター内部では、その変数やメンバーのアドレスに、代入先の変数やメンバーの型のサイズ分の 0
or 1
のデータが格納されることになります。そして、その 0
or 1
の一連のデータは表現の仕方によって異なるデータとして扱うことが出来ます。
前述のとおり、バイト単位の方が人間にとって扱いやすく、バイト単位で考えることも多いのですが、いずれにせよ、ここで重要なのは、コンピューター内部で管理されるデータは意味のない単なるビットのデータの集合 or バイトのデータの集合であるという点になります。そして、それらのデータに表現の仕方・扱い方というエッセンスを加えることで、これらのデータの集合が意味のある “整数” であったり、”浮動小数点数” であったり、”文字” であったり “色” に変化するのです。
共用体において、その表現の仕方を指定するのが各メンバーの “型” となります。異なる型のメンバーを共用体の型に持たせておけば、1つの共用体の型の変数で管理するデータは1つのみではあるものの、様々な表現の仕方でデータを扱うことが可能となります。そして、どのメンバーからデータにアクセスするかを指定するだけで、そのメンバーの型に応じた値の出力や値の代入等を行うことが可能となります。
つまり、共用体を利用すれば、メンバーを使い分けるだけで同じ1つのデータを異なる表現の仕方にすることができます。これは、異なる型への変換が容易に行えることを意味します。
例えばですが、共用体を利用すれば、先ほども説明したように特定の浮動小数点数のコンピューター内部での管理のされ方を簡単に確認することができます。
このような、浮動小数点数のコンピューター内部での扱い方を調べるというのはマニアックな共用体の使い方であって、実際のプログラム開発では、このような目的で共用体を利用することは少ないと思います。ただ、unsigned char
型の配列をメンバーに用意しておけば、単なるバイトのデータの集合を、他のメンバーの型に応じた様々な形式のデータに一瞬で変換することもできますし、逆に意味のある構造体等のデータを単なるバイトのデータに変換するようなことも可能です。そして、こういったテクニックは通信などを行う際にはよく利用されるので、こういった使い方が可能であることは覚えておくと良いと思います。
スポンサーリンク
サイズが節約可能
共用体を利用するメリットとして、プログラムの使用メモリサイズが節約可能である点も挙げられます。おそらく、共用体のメリットとして一番に挙げられるのはこの点になるのではないでしょうか?
少ないメモリでたくさんのデータが管理できるというわけではない
ただ、勘違いしないで欲しいのは、共用体に複数のメンバーを持たせておくことで、少ないメモリでたくさんのデータを管理できるというわけではないという点になります。前述でも説明しましたが、1つの共用体の型の変数で管理できるデータは1つのみです。なので、どれだけたくさんのメンバーを持たせたとしても、1つの共用体の型の変数で管理できるデータは1つのみで、少ないメモリでたくさんのデータが管理できるというわけではありません。
なんですが、共用体には1つのデータに対して複数の扱い方を行うことができるという特徴があり、これを上手く利用することでメモリの節約を実現することが可能です。
これに関しては、具体例で考えるのが理解しやすいと思いますので、具体例を挙げて説明していきたいと思います。
共用体で使用メモリの節約を行う具体例
例えば、下記のような2つの構造体(S_STUDENT
と S_EMPLOYEE
)について考えたいと思います。
typedef enum {
E_ELEMENTARY, // 小学生
E_JUNIOR_HIGH, // 中学生
E_SENIOR_HIGH, // 高校生
E_UNIVERSITY, // 大学生
} E_SCHOOL_TYPE;
typedef struct {
char name[256]; // 名前
unsigned int age; // 年齢
E_SCHOOL_TYPE school; // 学校の種類
unsigned int grade; // 学年
char club[256]; // 所属クラブ名
} S_STUDENT;
typedef struct {
char name[256]; // 名前
unsigned int age; // 年齢
char company[256]; // 会社名
char profession[256]; // 職種名
unsigned int salary; // 給料
} S_EMPLOYEE;
ちなみに、E_SCHOOL_TYPE
は列挙型(enum
型)であり、この列挙型に関しては下記ページで解説していますので、詳しく知りたい方は下記ページを参照していただければと思います。
上記の2つの構造体において、S_STUDENT
は学生を表現する構造体、S_EMPLOYEE
は会社員を表現する構造体となっています。それぞれの特徴を表すメンバーを持たせているつもりです。
これらの型のサイズは、環境によって異なる可能性もありますが、私の PC では S_STUDENT
が 524
バイト、S_EMPLOYEE
が 776
バイトとなります。ここからは、各構造体がこれらのサイズであることを前提に解説していきます。
さて、ここで、合計で 100
人分の学生 or 会社員の情報を管理するプログラムを開発することを考えたいと思います。合計で 100
人分であることは確定していますが、管理する学生や会社員は無作為に抽出するものとし、それぞれの人数はプログラム開発時には未確定であるとしたいと思います。
そして、これらの学生やサラリーマンを配列で管理するとすれば、両方とも最大で 100
人である可能性があるため、サイズ 100
の S_STUDENT
と S_EMPLOYEE
の配列を宣言しておくことが一番シンプルな管理方法になると思います。
S_STUDENT students[100];
S_EMPLOYEE employees[100];
このとき、S_STUDENT
の型のサイズは 524
バイトなので、サイズ 100
の配列では 52400
バイトのメモリが使用されることになります。同様に、S_EMPLOYEE
の型のサイズは 776
バイトなので、サイズ 100
の配列では 77600
バイトのメモリが使用されることになります。つまり、これらの配列の合計で 130000
バイトのメモリが必要になることになります。
で、このメモリ使用量を節約するのに役立つのが共用体となります。
結論としては、下記のように構造体と共用体を定義して配列を宣言してやれば、100
人分の学生 or 会社員のデータを管理することが出来るようになります。この時に必要な配列のメモリは 78000
のみとなり、先ほど説明して算出した 130000
よりも必要なメモリを節約できることになります。
typedef enum {
E_STUDENT,
E_EMPLOYEE,
E_OCCUPATION_MAX
} E_OCCUPATION_TYPE;
typedef enum {
E_ELEMENTARY, // 小学生
E_JUNIOR_HIGH, // 中学生
E_SENIOR_HIGH, // 高校生
E_UNIVERSITY, // 大学生
} E_SCHOOL_TYPE;
typedef struct {
char name[256]; // 名前
unsigned int age; // 年齢
E_SCHOOL_TYPE school; // 学校の種類
unsigned int grade; // 学年
char club[256]; // 所属クラブ名
} S_STUDENT;
typedef struct {
char name[256]; // 名前
unsigned int age; // 年齢
char company[256]; // 会社名
char profession[256]; // 職種名
unsigned int salary; // 給料
} S_EMPLOYEE;
typedef union {
S_STUDENT student;
S_EMPLOYEE employee;
} U_OCCUPATION;
typedef struct {
E_OCCUPATION_TYPE occupation_type;
U_OCCUPATION occupation;
} S_PERSON;
S_PERSON persons[100];
ポイントはやっぱり共用体の型として定義している U_OCCUPATION
になります。U_OCCUPATION
は S_STUDENT
の型のメンバーと S_EMPLOYEE
の型のメンバー変数を持っていますので、共用体の型のサイズはメンバーの最大サイズとなる で説明したように、これらのサイズの大きい方が U_OCCUPATION
のサイズとなります。具体的には S_EMPLOYEE
の型のサイズである 776
バイトが U_OCCUPATION
のサイズとなります。
また、U_OCCUPATION
の変数で管理できるデータの数は結局1つのみではあるのですが、 S_STUDENT
の型のメンバーと S_EMPLOYEE
の型のメンバーを持っていますので、1つのデータを S_STUDENT
の型のデータとしても S_EMPLOYEE
の型のメンバーとしても扱うことが出来ます。つまり、U_OCCUPATION
の1つの変数で扱えるデータは1つのみではあるものの、そのデータは S_STUDENT
でも S_EMPLOYEE
でも良い抽象的な変数となります。
どちらの型でも扱うことが出来るため、S_STUDENT
の型のデータと S_EMPLOYEE
の型のデータの数の合計が 100
なのであれば、U_OCCUPATION
型の配列の場合はサイズは 100
で済むことになります。とにかくどちらのデータも U_OCCUPATION
型の配列に格納し、後は扱い方を変えてやれば良いだけです。異なる型へのデータ変換が容易 で説明したように、共用体を利用すれば型の変換は容易に行うことができます。
ただし、U_OCCUPATION
型の配列に単に S_STUDENT
の型のデータ or S_EMPLOYEE
の型のデータを格納した場合、後からどの要素がどちらの型のデータであるかが分からなくなってしまいます。そのため、どの要素がどちらの型のデータであるかを判別するためのデータも管理してやった方がよいでしょう。
上記では、それを管理するために S_PERSON
を定義しています。この構造体の occupation
メンバーは U_OCCUPATION
型で、前述のとおり、この occupation
では S_STUDENT
の型のデータも S_EMPLOYEE
の型のデータも管理することが可能です。さらに、E_OCCUPATION_TYPE
型のメンバー occupation_type
も持たせており、このメンバーで occupation
で実際にどちらの型のデータを管理しようとしているかを判別できるようにしています。
具体的には、S_PERSON
の型の変数や配列の要素の occupation
にデータをセットする際、そのデータが S_STUDENT
の型のデータの場合に occupation_type
に E_STUDENT
をセットし、そのデータが S_EMPLOYEE
の型のデータの場合に occupation_type
に E_EMPLOYEE
をセットしてやれば、上の図のように後から occupation_type
を参照して occupation
にセットされているデータが S_STUDENT
or S_EMPLOYEE
のどちらであるかを判別することができるようになります。
ということで、単にデータを記録するだけであれば U_OCCUPATION
型で十分ですが、後からデータを扱う際には、S_PERSON
のように “どの型としてデータを扱えば良いかを判別するためのメンバー” を持たせた構造体の型を用意しておくのが良いと思います。
そして、この場合でも S_PERSON
のサイズは 780
となり、100
人分の学生 or 会社員のデータを管理するために必要になるメモリ使用量は 78000
となります。単に2つの構造体でデータを管理する時の 130000
よりも大きくサイズを削減することが可能となります。
単に1つの種類のデータを管理する場合はハッキリ言って共用体を使うメリットはほとんどないですが、上記の例のように複数の種類のデータを管理し、さらに動的に管理する種類のデータが変化するような場合は共用体を利用することで必要なメモリ使用量を大きく削減することができます。そのため、特に使用可能なメモリ量が少ない環境での開発では、この共用体が活躍します。
汎用的な関数が作成可能
また、共用体のメリットとして「汎用的な関数が作成可能」である点も挙げられます。
C言語では、関数を定義する際には具体的な仮引数の型を指定する必要があります。そして、基本的には、関数呼び出し時には仮引数の型に合わせた実引数を指定して関数を呼び出す必要があります(もちろん、無理やり仮引数とは異なる型の実引数を指定することも可能ですが、その場合は変に型変換が行われて意図通りに関数が動作しなかったりバグの原因になったりする可能性が高いです)。
ここまでの説明にも登場した S_STUDENT
と S_EMPLOYEE
の例で具体的に考えてみたいと思います。例えば、S_STUDENT
の型のデータや S_EMPLOYEE
の型のデータを引数として受け取り、引数として受け取ったデータの各種メンバーの値を出力する関数を定義したいとします。
共用体を利用しない場合の関数定義
この場合、下記のように2つの関数を別々に定義する方が多いのではないかと思います。S_STUDENT
と S_EMPLOYEE
は比較的似ている構造体ではあるのですが、関数の仮引数に型を具体的に指定する必要があるため、下記のように構造体の型ごとに関数を定義する必要があります。まぁ、このように定義するのも悪くはないのですが、構造体の型が増えるたびに関数を追加する必要があって少し面倒ですね…。
void printStudent(S_STUDENT *student) {
printf("name : %s\n", student->name);
printf("age : %d\n", student->age);
printf("school : %d\n", student->school);
printf("grade : %d\n", student->grade);
printf("club : %s\n", student->club);
}
void printEmployee(S_EMPLOYEE *employee) {
printf("name : %s\n", employee->name);
printf("age : %d\n", employee->age);
printf("company : %s\n", employee->company);
printf("profession : %s\n", employee->profession);
printf("salary : %d\n", employee->salary);
}
ここまでの解説を読んでくださった方であれば既に察してくださっているかもしれないですが、共用体を利用することで上記の2つの関数を1つにまとめることができます。
共用体を利用する場合の関数定義
今回の例であれば、U_OCCUPATION
型を関数の仮引数の型に指定しておけば、U_OCCUPATION
型のデータを関数が受け取ることができるようになります。そして、その U_OCCUPATION
型のデータを S_STUDENT
型のデータとして扱いたいのであれば U_OCCUPATION
型のデータの持つ student
メンバーを利用し、S_EMPLOYEE
型のデータとして扱いたいのであれば U_OCCUPATION
型のデータの持つ employee
メンバーを利用するようにしてやれば、1つの仮引数で2つの型のデータを扱うことができることになります。
異なる型へのデータ変換が容易 で説明したように、共用体では1つのデータしか扱えませんが、その1つのデータの扱い方は容易に変更することが可能です。
下記は、仮引数を U_OCCUPATION *
型とすることで、構造体 S_STUDENT
と S_EMPLOYEE
の各メンバーを出力する関数の例となります。
void printOccupation(U_OCCUPATION *occupation) {
if (S_STUDENTで扱いたい場合) {
S_STUDENT student = occupation->student;
printf("name : %s\n", student.name);
printf("age : %d\n", student.age);
printf("school : %d\n", student.school);
printf("grade : %d\n", student.grade);
printf("club : %s\n", student.club);
} else if (S_EMPLOYEEで扱いたい場合) {
S_EMPLOYEE employee = occupation->employee;
printf("name : %s\n", employee.name);
printf("age : %d\n", employee.age);
printf("company : %s\n", employee.company);
printf("profession : %s\n", employee.profession);
printf("salary : %d\n", employee.salary);
}
}
ただ、上記で if
文の条件が日本語になってしまっているように、単に U_OCCUPATION
型のデータを受け取るだけだと、そのデータを S_STUDENT
と S_EMPLOYEE
のどちらで扱えば良いかが関数内部で判断できません。なので、関数内でデータを S_STUDENT
と S_EMPLOYEE
のどちらで扱えば良いかを判断するためのデータが追加で必要になります。
例えば、前述でも紹介した S_PERSON
であれば、U_OCCUPATION
型 の occupation
メンバーを持っており、このメンバーのデータは S_STUDENT
と S_EMPLOYEE
の両方の型のデータとして扱うことが可能です。さらに、S_PERSON
は occupation
メンバーをどちらの型で扱うべきかを判断するためのメンバーとして occupation_type
メンバを持っています。
そのため、関数で S_PERSON
型を受け取るようにすることで、occupation
メンバーを適切な型に変換することができるようになります。そして、その関数の例が下記となります。この関数であれば、1つの引数から2つの構造体の型のデータを適切に扱うことが可能です。
void printPerson(S_PERSON *person) {
if (person->occupation_type == E_STUDENT) {
S_STUDENT student = person->occupation.student;
printf("name : %s\n", student.name);
printf("age : %d\n", student.age);
printf("school : %d\n", student.school);
printf("grade : %d\n", student.grade);
printf("club : %s\n", student.club);
} else if (person->occupation_type == E_EMPLOYEE) {
S_EMPLOYEE employee = person->occupation.employee;
printf("name : %s\n", employee.name);
printf("age : %d\n", employee.age);
printf("company : %s\n", employee.company);
printf("profession : %s\n", employee.profession);
printf("salary : %d\n", employee.salary);
}
}
このように、関数の仮引数に共用体の型 or 共用体の型をメンバーに持つ構造体を指定すれば、そのメンバーから各型のメンバーにアクセスすることで、関数内部で複数の型のデータを扱うことが出来るようになります。これにより、1つの引数に対して1つの型のデータではなく複数の型のデータを扱うことが可能な関数を実現することができ、関数の汎用性を向上させることができます。
抽象的な型が実現可能
先ほどの 汎用的な関数が作成可能 で示したようなメリットが得られるのは、共用体を利用することで「抽象的な型」が実現可能となるからになります。
汎用的な関数が作成可能 では S_STUDENT
と S_EMPLOYEE
の抽象的な型として U_OCCUPATION
を導入し、この U_OCCUPATION
を関数の仮引数の型とすることで、U_OCCUPATION
の具体的な型となる S_STUDENT
と S_EMPLOYEE
の両方のデータを扱うことが可能な関数を実現しています。
このように、プログラムでは型を抽象化することで様々なメリットが得られることが多いです。
この型の抽象化は、イメージとしては他のプログラミング言語におけるサブクラスとスーパークラス(子クラスと親クラス)の関係に似ています。そして、この型の抽象化は、C言語においては共用体を利用することで実現することが可能です。これは、異なる型へのデータ変換が容易 で説明したように、共用体が1つのデータを複数のメンバーで扱うことができ、それによって他の型への変換が容易に行えるという特徴を持つからになります。結局、1つのデータを複数のメンバーで扱うことができるという一見意味のなさそうな特徴が、実は共用体の特徴の最大のポイントであり、最大の特徴になります。
具体的には、複数の具体的な構造体の型のメンバーを持つ共用体の型を定義することで、この抽象化を実現することが出来ます。ここまでの例であれば、U_OCCUPATION
がこれにあたります。
そして、U_OCCUPATION
の変数は S_STUDENT
や S_EMPLOYEE
を抽象化した変数と考えられます。そして、この U_OCCUPATION
の変数の stundent
メンバーや employee
メンバーを利用することで、具体化した型である S_STUDENT
や S_EMPLOYEE
のデータを扱うことができるようになります。
ただし、実際には、共用体の型の変数や配列の要素がどの型のデータであるかを判別できるようにするメンバーを持たせる必要があるため、共用体の型のメンバーを持つ構造体を別途定義することが多いと思います。ここまでの例であれば、これは S_PERSON
がこれにあたります。
ここまで説明してきたように、共用体 or 共用体の型のメンバーを持つ構造体を導入することで、型の抽象化を行い、よりハイレベルなコーディングができるようになります。是非、この抽象化も意識して実装等に取り組んでみていただければと思います!
スポンサーリンク
まとめ
このページでは、C言語における共用体(union
)について説明しました!
共用体と構造体は、使い方に関しては似ていますが、これらは全く異なるデータの構造になります。
これらの決定的な違いは、構造体がメンバーの数だけのデータを管理できるのに対し、共用体では管理できるデータの数が1つのみである点になります。構造体の場合は、メンバーごとに異なるデータが存在し、異なるメンバーからは異なるデータにアクセスすることになります。それに対して共用体では管理するデータが1つのみで、その1つのデータを各メンバーの型に応じたデータとして扱うことができます。
共用体を利用することで使用メモリを削減できると考えている人もおられるかもしれませんが、1つのデータを少ないメモリで扱うことが出来るというわけではありません。圧縮など特別なことをしない限り、管理できるデータのサイズや数に応じて必要なメモリは決まっています。これはコンピューター上でデータを扱うときの原則であり、共用体を利用してもこれは変わりません。
ですが、使い方次第では共用体の導入によって使用できるメモリを削減できることもあります。具体的には、動的に扱うデータの型が異なるような場合です。
また、共用体を利用することで型を抽象化することも可能です。そして、これによって簡潔で読みやすく、さらにメンテナンス性の高いソースコードを実現することが可能です。
共用体の使い方や特徴は理解している人は多いかもしれませんが、共用体のメリットをしっかり理解している人は少ないのではないのかと思います。なので、使ったことが無い方も多いと思います。是非、このページで読んだ内容を頭の片隅にでもとどめておいていただき、設計や実装をしているときに共用体のメリットが活かせそうと感じた場合には、是非共用体を利用してみてください!
オススメの参考書(PR)
C言語学習中だけど分からないことが多くて挫折しそう...という方には、下記の「スッキリわかるC言語入門」がオススメです!
まず学習を進める上で、参考書は2冊持っておくことをオススメします。この理由は下記の2つです。
- 参考書によって、解説の仕方は異なる
- 読み手によって、理解しやすい解説の仕方は異なる
ある人の説明聞いても理解できなかったけど、他の人からちょっと違った観点での説明を聞いて「あー、そういうことね!」って簡単に理解できた経験をお持ちの方も多いのではないでしょうか?
それと同じで、1冊の参考書を読んで理解できない事も、他の参考書とは異なる内容の解説を読むことで理解できる可能性があります。
なので、参考書は2冊持っておいた方が学習時に挫折しにくいというのが私の考えです。
特に上記の「スッキリわかるC言語入門」は、他の参考書とは違った切り口での解説が豊富で、他の参考書で理解できなかった内容に対して違った観点での解説を読むことができ、オススメです。題名の通り「なぜそうなるのか?」がスッキリ理解できるような解説内容にもなっており、C言語入門書としてもかなり分かりやすい参考書だと思います。
もちろんネット等でも色んな観点からの解説を読むことが出来ますので、分からない点は別の人・別の参考書の解説を読んで解決していきましょう!もちろん私のサイトも参考にしていただけると嬉しいです!
入門用のオススメ参考書は下記ページでも紹介していますので、こちらも是非参考にしていただければと思います。
https://daeudaeu.com/c_reference_book/