C言語教室 第17回 - 共用体とは何か
構造体に似ているけれど、ぜんぜん用途も目的も違う共用体というものがあります。
整数と文字のメンバ変数を持つ構造体を定義すると
struct iandc {
int member_int;
char member_char;
};
となり、この構造体の変数を宣言すれば、その変数には整数型の値「と」文字が格納できます。
同じように整数と文字のメンバ変数を持つ共用体は以下のように定義します。
union iandc {
int member_int;
char member_char;
};
構造体と同じように、この共用体の変数を宣言すると、その変数には整数型の値「または」文字が格納できるのです。
つまり member_int に値を代入してから、member_char にも値を代入すると、member_int の値は書き換えられてしまうんです。どちらのメンバ変数で共用体を使っているかはプログラムを書く人が適切に指定しなければなりません。
#include <stdio.h>
union iandc {
int member_int;
char member_char;
};
void main() {
union iandc uic;
uic.member_int = 0;
uic.member_char = '0';
printf("%d\n", uic.member_int);
}
48
このプログラムを走らせる環境によっては、異なる結果が出力されることもあります。
共用体
どうしてこんなものが存在するのかというと、いくつかの理由が考えられます。
ひとつめが本当に、同じ変数にいろいろな型の値を格納したいという場合です。いわゆる型の無い言語での使い方であったり、VB/VBAのVariant型のような使い方がしたいときです。先の union iandc という共用体変数を宣言すれば、この変数には整数も文字も代入できます。単独の変数では効果が分かりにくいかもしれませんが、配列として使うと効果が実感できるかもしれません。
union iandc[10];
int i;
for (i = 0; i < 10; i++) {
if (i % 2 == 0)
iandc[i].member_int = i;
else
iandc[i].member_char = ‘0’ + (char)i;
}
これで、偶数番目の要素には整数が、基数番目の要素には文字が代入されます。この使い方では、どの要素がどの型で使われているのかは判らないので、一般的には、どの型が入っているかのタグを付けて使うことが多いです。
#include <stdio.h>
union iandc {
int member_int;
char member_char;
};
struct iandc {
int vartype;
union iandc value;
};
void main() {
struct iandc v[10];
int i;
for (i = 0; i < 10; i++) {
if (i % 2 == 0) {
v[i].vartype = 0;
v[i].value.member_int = i;
} else {
v[i].vartype = 1;
v[i].value.member_char = '0' + (char)i;
}
}
for (i = 0; i < 10; i++) {
switch (v[i].vartype) {
case 0:
printf("v[%d]=%d\n", i, v[i].value.member_int);
break;
case 1:
printf("v[%d]=’%c’\n", i, v[i].value.member_char);
break;
}
}
}
v[0]=0
v[1]='1'
v[2]=2
v[3]='3'
v[4]=4
v[5]='5'
v[6]=6
v[7]='7'
v[8]=8
v[9]='9'
なお、構造体と共用体は名前空間が異なるので、同じ名前の型を宣言できます。
次が同じ値を他の型として読み書きしたい場合です。エンディアンの変更をビットシフト演算で行うこともありますが、共用体を使うことも出来ます。
#include <stdio.h>
union shortint {
short i;
char b[2];
};
void main() {
union shortint v;
v.i = 515;
printf("%02x:%02x\n", v.b[0], v.b[1]);
}
03:02
short は 2バイトと仮定しています。実行する環境によっては異なる値になると思います(この結果がでるのはリトルエンディアン)。処理系のエンディアンに関わらずネットワークバイトオーダーはビッグエンディアンなので、それなりに変換する状況は発生します。
エンディアン
[バイトオーダー]ビックエンディアン/リトルエンディアン
それから異なる型のポインタを同じ変数に格納したい場合です。
#include <stdio.h>
union ptr_iandd {
int *pi;
double *pd;
};
void variable_print(union ptr_iandd p, int value_type) {
switch (value_type) {
case 1:
printf("%d\n", *(p.pi));
break;
case 2:
printf("%f\n", *(p.pd));
break;
}
}
void main() {
union ptr_iandd p;
int i = 10;
double d = 100.0;
p.pi = &i;
variable_print(p, 1);
p.pd = &d;
variable_print(p, 2);
}
10
100.000000
異なる型へのポインタは、いずれにせよアドレスが入っていることには違いないので、都度、適切な型変換(キャスト)することで使えるのですが、共用体を定義して、どの型へのポインタであるのかをメンバ名で指定することで、より安全に使うことが出来ます。もちろん構造体として宣言してタグを付けることで、データに型情報も入れて使うこともできます。
一部だけが異なる構造体を使いたい場合があります。どういう事かというと、ある人の個人情報を扱いたいとして、その人が学生であれば学校の種類と学年、働いていれば雇用形態と勤務年数を格納できるようにしたいとします。このような場合に一部のメンバ変数を切り替えて使いたいことがあります。これは Pascal では可変レコード型という型があるのですが、C言語でも構造体と共用体を組み合わせることで同じ機能を実現できます。
<7> レコード型 (標準 Pascal 範囲内での Delphi 入門)
#include <stdio.h>
#define SCHOOL_ELEMENTRAY 1
#define SCHOOL_MIDDLE 2
#define SCHOOL_HIGH 3
struct student {
int school_type;
int grade;
};
#define EMPLOYMENT_PERMANENT 1
#define EMPLOYMENT_CONTRACT 2
struct employment {
int status;
int length;
};
union extent_info {
struct student school_info;
struct employment empoly_info;
};
#define STUDENT 1
#define EMPLOYER 2
struct personal_info {
int id;
char name[32];
int extent_type;
union extent_info info;
};
void print_friend(struct personal_info *p) {
printf("%03d:%s:", p->id, p->name);
switch (p->extent_type) {
case STUDENT:
switch (p->info.school_info.school_type) {
case SCHOOL_ELEMENTRAY:
printf("ELEMENT_SCHOOL");
break;
case SCHOOL_MIDDLE:
printf("MIDDLE SCHOOL");
break;
case SCHOOL_HIGH:
printf("HIGH SCHOOL");
break;
}
printf(":%d\n", p->info.school_info.grade);
break;
case EMPLOYER:
switch (p->info.empoly_info.status) {
case EMPLOYMENT_PERMANENT:
printf("PERMANENT");
break;
case EMPLOYMENT_CONTRACT:
printf("CONTRACT");
break;
}
printf(":%d\n", p->info.empoly_info.length);
}
}
void main() {
struct personal_info friend;
friend.id = 1;
friend.name = "John Smith";
friend.extent_type = EMPLOYER;
friend.info.empoly_info.status = EMPLOYMENT_PERMANENT;
friend.info.empoly_info.length = 12;
print_friend(&friend);
friend.id = 2;
friend.name = "Susan Miller";
friend.extent_type = STUDENT;
friend.info.school_info.school_type = SCHOOL_HIGH;
friend.info.school_info.grade = 2;
print_friend(&friend);
}
001:John Smith:PERMANENT:12
002:Susan Miller:HIGH SCHOOL:2
構造体と共用体が入れ子になっているので、メンバ名の指定が多段階になりますね。間違えないようにするのがちょっと大変です。このあたりは統合開発環境だと選択肢を示してくれることもあるのですが、普通のエディタだと混乱することもあります。そこで型名は_t で終わるとか、メンバ名は m_ で始まるとか命名規則を使うことでわかりやすくする人も多いです。
このように同じデータを異なる形式で扱うという方法は、実に古くからあるのですが、どの時代であっても決して分かりやすいものではなく、具体的にデータがどのようにメモリに格納されているのかを意識する必要が出てきます。C言語の場合、構造体であれば先頭のメンバ変数は構造体全体と同じアドレス、次のメンバは例えば先頭+8のアドレスにあり、共用体はどのメンバであっても共用体全体と同じアドレスにあるわけです。
FORTRANプログラマーのためのC(1)
話はそう単純ではなく、必ずしも先頭から詰まった状態でメンバ変数が配置されるわけではないのです。CPUにはアクセスしやすいアドレスとアクセスしにくいアドレスがあり、これはだいたいにおいてCPUのビット数に対応したバイト毎(32ビットCPUであれば4バイト毎)がアクセスしやすいので、それ以下のサイズのメンバがあっても、キリの良いサイズを占めるように配置します。ですからメンバ変数のサイズの合計が構造体(や共用体)のサイズになるわけではありません。これは sizeof で大きさを出してみれば確認できます。
【C言語】構造体を作る上でのアライメントのお話。そもそもアライメントとは...
これは異なる処理系や特定のハードウェアの要求に合わせるためには困ることもあって、隙間(パディングと呼びます)を空けないで配置してほしいこともあるわけです。そのような時には、gcc であれば
struct s0 {
char a, b, c;
} __attribute__ ((packed));
もっと古いコンパイラであれば
#pragma pack(1)
もしくはコンパイルオプションなどで指定します。
いやいや、だんだんややこしい話になってしまいましたね。もっとも普通に使っている限り、パディングにより動作は変わらないはずです(メモリ使用量やパフォーマンスは変わりますが)。
次回は、構造体のビットフィールドと typedef による型宣言です。
前回の教室の課題は、今回の課題と一緒に解答します。
ヘッダ画像は、いらすとや さんよりhttps://www.irasutoya.com/2017/01/blog-post_66.html