C言語編 第55章 共用体

先頭へ戻る

この章の概要

この章の概要です。

共用体

共用体は、あるメモリ領域を、異なる型で使いまわすことができるというものです。

共用体自身も型であり、共用体型と呼ばれます。構造体型や列挙型を使うときと同様、まずは共用体型の定義を記述し、その型の変数を宣言するなどして使用します。

共用体型の定義は次のように行います。

union タグ名 {
    型 メンバ名;
    型 メンバ名;
      :
};

共用体を表すキーワードは union です。それ以外の形は構造体型の定義とまったく同じです。

「タグ名」には、タグ(共用体タグ)に付ける名前を記述します。構造体のタグと同じで、複数の共用型を区別するために使う名前です。共用体型の名前を使うときには「union タグ名」のように記述します。

「タグ名」は省略することが可能です。ただし省略してしまうと、それ以降「union タグ名」の形の記述ができなくなってしまうため、使い方が限定されます。

「型 メンバ名;」の部分についても構造体型のときと同じです。初期化子を置くことはできませんし、static などのキーワードも付けられません。また、共用体のメンバの名前は、その共用体型に所属するものであって、他の場所に存在する名前と被っても問題ありません

定義の仕方は構造体とよく似ていますが、共用体では、それぞれのメンバが、同じメモリアドレスを共有する点が異なります。次の2つの定義を比べてみます。

struct S_tag {
    int    num;
    double d;
    char   str[10];
} s;

union U_tag {
    int    num;
    double d;
    char   str[10];
} u;

このように同じメンバで構成される構造体型と共用体型を定義したとして、それぞれの変数を定義したとします。すると、メモリ上のイメージは次のように異なったものになります(sizeof(int) == 4、sizeof(double) == 8、アラインメント 8 を想定)。

構造体のメモリイメージ 共用体のメモリイメージ

構造体の方は知っての通り、各メンバが順番にメモリ上に配置されていきます。したがって、各メンバが使うメモリ領域は分かれています。メンバのメモリアドレスを調べれば、当然すべて異なります。

一方、共用体の方は、各メンバが使うメモリ領域の先頭が揃っており、同じメモリ領域を部分的に共有しています。こんなことが可能であるはずがないと思うかも知れませんが、もちろん条件はあって、共用体のメンバは同時には使えません。ある1つの共用体について、ある瞬間に値を持っているのは、どれか1つのメンバだけなのです。ですから、使いどころはそれなりに限られてきます。

なお、構造体の場合はメンバ間や最後のメンバの終わりに、共用体の場合は一番大きいメンバが使う領域の後ろに、パディング(第26章)が加わることがあります。先ほどのイメージ図にもパディングを入れてあります(ここでは 8 の倍数のバイト数に調整されることを想定しています。実際には、これとは異なる入り方をするかも知れません)。

このイメージ図の通り、構造体型の大きさは、すべてのメンバの大きさを足し合わせたもの+パディングですし、共用体型の大きさは、一番大きいメンバの大きさ+パディングです

#include <stdio.h>

struct S_tag {
    int    num;
    double d;
    char   str[10];
};

union U_tag {
    int    num;
    double d;
    char   str[10];
};

int main(void)
{
    printf( "struct: %u\n", sizeof(struct S_tag) );
    printf( "union: %u\n", sizeof(union U_tag) );

    return 0;
}

実行結果:

struct: 32
union: 16

また、共用体の各メンバのメモリアドレスは同一ですし、共用体型の変数自体のメモリアドレスを取っても、やはり同じになります

#include <stdio.h>

union U_tag {
    int    num;
    double d;
    char   str[10];
};

int main(void)
{
    union U_tag u;

    printf( "%p\n", &u );
    printf( "%p\n", &u.num );
    printf( "%p\n", &u.d );
    printf( "%p\n", u.str );

    return 0;
}

実行結果:

006FFE84
006FFE84
006FFE84
006FFE84

共用体変数の初期化

共用体型は、型定義の際の見た目に反して、実際的には要素が1つしかありません。ですから、構造体型や配列型のような集成体ではないのですが、初期化のルールは似ています。

明示的に初期値を与えなかった場合、自動記憶域期間を持つのならメンバは不定値であり、静的記憶域期間を持つのなら、メンバは暗黙的に初期化されます第22章)。暗黙的に初期化される場合は、先頭に宣言したメンバに対して行われます。

共用体変数の宣言と同時に初期値を与えるには、次のように書きます。

union タグ名 変数名 = { 初期化子 };

同時に使えるメンバは1つなので、初期化子は常に1つだけです。また、先頭に宣言したメンバに対してしか初期化できません。

C99 では、要素指示子を使うことによって、任意のメンバに対して初期化できます。

union Data_tag {
    int num;
    char c[4];
};

int main(void)
{
    union Data_tag data = { 123 };
    
    return 0;
}

また、自動記憶域期間を持つ場合は、同じ型の別の共用体変数を使って初期化できます。この場合、元の共用体変数と同じ値を持った状態に初期化されます。

void f(void)
{
    union Data_tag data1 = { 123 };
    union Data_tag data2 = data1;
    static union Data_tag data2 = data1;  /* コンパイルエラー(自動記憶域期間を持たないため)*/
}

C99 (要素指示子)

C99 で追加された要素指示子を使って、特定のメンバを選んで初期値を与えることができます。

#include <stdio.h>

union Data_tag {
    int num;
    char c[4];
};


int main(void)
{
    union Data_tag data = { .c = "abcd" };

    printf( "%c%c%c%c\n", data.c[0], data.c[1], data.c[2], data.c[3] );

    return 0;
}

実行結果:

abcd

共用体変数 data の初期化のところを見て下さい。「.c = "abcd"」という記述によって、c というメンバに初期値 "abcd" が与えられます。このように、「.メンバ名 = 初期値」という構文が使えます。

基本的な使い方

では、実際に使ってみます。

#include <stdio.h>
#include <string.h>

union Data_tag {
    int num;
    char c[4];
};


int main(void)
{
    union Data_tag data = { 123 };

    printf( "%d\n", data.num );

    memcpy( data.c, "abcd", 4 );
    printf( "%c%c%c%c\n", data.c[0], data.c[1], data.c[2], data.c[3] );

    return 0;
}

実行結果

123
abcd

共用体のメンバの参照は、構造体と同じように .演算子で行います。ポインタ経由の場合には、->演算子が使える点も同様です

このサンプルプログラムでは、共用体Data_tag は 4バイトの大きさを持つというつもりで定義しました。その 4バイトのメモリ領域を、int型 (num) としても扱えるし、要素数4 の char型配列 (c) としても扱えるようになっています。

このような共用体の使い方をすると、整数と文字列が混在するようなデータ表を少ないメモリで実現できます。構造体で実現すると、一方のメンバを使った場合には他方のメンバはまったく未使用なままになってしまい、無駄なメモリを使ってしまいます。

共用体の使い方として注意しなければならないのは、最後に値を入れたメンバからしか、値を正しく取得できる保証がないという点です。例えば、data.num に代入した直後で data.c の値を調べると、どんな結果が返ってくるか分かりません。同じメモリ領域を共有しているのだから、以下のコードで 'a' が出力されるように思えますが、その保証はありません。

data.num = 'a';
printf( "%c\n", data.c[0] );

しかし現実には、このような使い方をしているプログラムは多くあります。特定のコンパイラではうまく動作するかも知れませんが、移植性がないプログラムです。

このように、最後にどのメンバへ値を入れたのかを意識してプログラムを書く必要があります。現在どのメンバの値が有効になっているかを知る手段があればよいのですが、そのような方法は用意されていません。そのため、使い方がやや複雑になる場合には、最後にどのメンバへ値を入れたのかを、自前で管理すると良いかも知れません。例えば、次のサンプルプログラムのように、共用体を構造体のメンバにして、構造体の側に管理用の変数を置く方法があります。

#include <stdio.h>
#include <string.h>
#include <assert.h>

enum ValueType_tag {
    VALUE_TYPE_INT,     /* int型 */
    VALUE_TYPE_STRING,  /* char型配列 */

    VALUE_TYPE_NUM
};

struct Data_tag {
    enum ValueType_tag value_type;  /* 最後に値を格納したときの型 */

    union Value_tag {
        int num;
        char c[4];
    } v;
};

void setIntValue(struct Data_tag* data, int value);
void setStringValue(struct Data_tag* data, const char* value);
void printValue(const struct Data_tag* data);

int main(void)
{
    struct Data_tag data;

    setIntValue( &data, 123 );
    printValue( &data );

    setStringValue( &data, "abcd" );
    printValue( &data );

    return 0;
}

/*
    int型として値を格納する
    引数:
        data:	構造体のアドレス
        value:	格納する値
*/
void setIntValue(struct Data_tag* data, int value)
{
    data->v.num = value;
    data->value_type = VALUE_TYPE_INT;
}

/*
    文字列として値を格納する
    引数:
        data:	構造体のアドレス
        value:	格納する値。末尾の '\0' を含めず4文字でなければならない。
*/
void setStringValue(struct Data_tag* data, const char* value)
{
    assert( strlen(value) == 4 );

    memcpy( data->v.c, value, 4 );
    data->value_type = VALUE_TYPE_STRING;
}

/*
    現在の型に応じて正しい値を出力する
    引数:
        data:	構造体のアドレス
*/
void printValue(const struct Data_tag* data)
{
    switch( data->value_type ){
    case VALUE_TYPE_INT:
        printf( "%d\n", data->v.num );
        break;

    case VALUE_TYPE_STRING:
        printf( "%c%c%c%c\n", data->v.c[0], data->v.c[1], data->v.c[2], data->v.c[3] );
        break;

    default:
        assert( !"型が不適切です。" );
        break;
    }
}

実行結果

123
abcd

構造体のメンバとして、共用体定義とその変数宣言を含めています。また、構造体のメンバには、列挙型変数が含まれています。

メモリ領域が共有されているのは、共用体の中にある num と c であって、列挙型変数の value_type は無関係であることに注意して下さい。つまり、num と c のどちらが有効なタイミングであっても、value_type を参照することは、常に問題のない行為です。

共用体変数への代入と、値の出力を関数化することで、常に列挙型変数value_type を使って適切なメンバが参照されるようになっています。もちろん、常にこれらの関数を経由するようにプログラムを書かないといけませんが、それを守っていれば正常な状態が保たれるはずです。

列挙型の変数が加わったことによって、構造体全体の大きさが増えてしまうので、これではメモリの節約効果がありませんが、共用体部分の大きさがもっと大きければ意味があります。

構造体を含む共用体

メモリ領域を共有したいメンバが1個だけならば、次のように、共用体型の定義内にメンバを書き並べればいいです。

union {
    int a;
    double b;
};

しかし、共有したいものが「複数のメンバの組み合わせ」の場合はどうすればいいでしょうか。例えば、int型の「a と b」あるいは、double型の「a と b」のような場合です。

そのような場合には、組み合わせの部分を構造体にします。

union U_tag {
    struct {
        int a;
        int b;
    } v1;
    struct {
        double a;
        double b;
    } v2;
};

共用体の型定義の中に書き並べるものは、あくまでメンバ(変数)の宣言であって、型の定義ではないので、構造体の型定義と同時に、v1 と v2 という名前でメンバの宣言も行っていることに注意して下さい。

a や b といったメンバを参照するには、「u.v1.a」だとか「u.v2.b」といったように、v1 や v2 を経由する必要があります。また、明示的に初期化を行うのなら、先頭のメンバに初期値を与えなければならないので、v1 の方に合わせて行うことになります。

#include <stdio.h>

union U_tag {
    struct {
        int a;
        int b;
    } v1;
    struct {
        double a;
        double b;
    } v2;
};

int main(void)
{
    union U_tag u = { {10, 20} };
    printf( "%d %d\n", u.v1.a, u.v1.b );
    
    u.v2.a = 3.5;
    u.v2.b = 5.5;
    printf( "%f %f\n", u.v2.a, u.v2.b );

    return 0;
}

実行結果

10 20
3.500000 5.500000

単にメンバの組み合わせを表現するためだけに構造体を定義しているので、構造体のタグ名を省略していますが、必要であれば書いても構いません。その場合は、構造体の型定義自体は外に出した方が分かりやすいかも知れません。

struct IntValues_t {
    int a;
    int b;
};
struct DoubleValues_t {
    double a;
    double b;
};
union U_tag {
    struct IntValues_t v1;
    struct DoubleValues_t v2;
};


練習問題

問題① 「幅」と「高さ」のペアを、int型、あるいは float型で管理できるような共用体を作成して下さい。


解答ページはこちら

参考リンク

更新履歴

'2018/5/21 全体的に内容を強化。
明示的な初期化の説明が間違っていたのを修正({ } が必要である)。
練習問題を差し替え。元の問題の内容は、本編での解説に昇格した。

'2018/5/19 ビットフィールドの話題を、第56章へ移動。
章のタイトルを変更(「共用体とビットフィールド」->「共用体」)

'2018/5/7 新規作成。第50章に含まれていた内容を移動してきて、手直し。





前の章へ(第54章 乱数)

次の章へ(第56章 ビットフィールド)

C言語編のトップページへ

Programming Place Plus のトップページへ


このエントリーをはてなブックマークに追加
rss1.0 取得ボタン RSS