[C言語入門]共用体でメモリを有効活用

今回は共用体です。
共用体の定義は以下のように行います。

union タグ名 {
    メンバ1;
    メンバ2;
    ...
};




一見すると構造体っぽい定義方法で、同じように見えるかもしれませんが、構造体と、共用体ではメモリ確保のされ方が異なります。

このことを確認するために、サンプルを1つ実行してみます。

#include <stdio.h>
 
struct t_CHARACTER {
    char c0;
    char c1;
    char c2;
    char c3;
};
 
union u_CHARACTER {
    char c0;
    char c1;
    char c2;
    char c3;
};
 
 
int main()
{
    struct t_CHARACTER data1;
    union u_CHARACTER  data2;
 
    data1.c0 = 'a';
    data1.c1 = 'b';
    data1.c2 = 'c';
    data1.c3 = 'd';
 
    data2.c0 = 'e';
    data2.c1 = 'f';
    data2.c2 = 'g';
    data2.c3 = 'h';
 
 
    printf( "\ndata1 --\n" );
    printf( "%c\n", data1.c0 );
    printf( "%c\n", data1.c1 );
    printf( "%c\n", data1.c2 );
    printf( "%c\n", data1.c3 );
 
    printf( "\ndata2 --\n" );
    printf( "%c\n", data2.c0 );
    printf( "%c\n", data2.c1 );
    printf( "%c\n", data2.c2 );
    printf( "%c\n", data2.c3 );
 
 
    printf( "\n" );
    printf( "\ndata1 address --\n" );
    printf( "%p\n", &data1.c0 );
    printf( "%p\n", &data1.c1 );
    printf( "%p\n", &data1.c2 );
    printf( "%p\n", &data1.c3 );
 
    printf( "\ndata2 address --\n" );
    printf( "%p\n", &data2.c0 );
    printf( "%p\n", &data2.c1 );
    printf( "%p\n", &data2.c2 );
    printf( "%p\n", &data2.c3 );
 
    return 0;
}



実行結果

data1 --
a
b
c
d
 
data2 --
h
h
h
h
 
 
data1 address --
0x28cc8c
0x28cc8d
0x28cc8e
0x28cc8f
 
data2 address --
0x28cc8b
0x28cc8b
0x28cc8b
0x28cc8b




構造体の場合は、各メンバ変数が異なるメモリ領域に割り当てられますが、共用体の場合は同じアドレスが使用されます(場所が”共用”されます)。上記サンプルのdata2ではc0~c3へ順に値をセットしていますが、これは同一番地への書き込みとなり、最終的にc3の値が有効になります。



上記の例だと、共用体のメリットがわからないかと思いますので、もう少し役に立ちそうな別の例を2つほど出してみます。

#include <stdio.h>
 
union u_test {
        int idata;
        unsigned char cdata[4];
};
 
int main()
{
        union u_test data2;
 
        data2.idata = 10;
 
 
        printf( "\ndata2(idata) --\n" );
        printf( "%p %d\n", &data2.idata, data2.idata );
 
        printf( "\ndata2(cdata) --\n" );
        printf( "%p %04x\n", &data2.cdata[0], data2.cdata[0] );
        printf( "%p %04x\n", &data2.cdata[1], data2.cdata[1] );
        printf( "%p %04x\n", &data2.cdata[2], data2.cdata[2] );
        printf( "%p %04x\n", &data2.cdata[3], data2.cdata[3] );
 
        return 0;
}



実行結果

data2(cdata) --
0x28cc8c 10
 
data2(cdata) --
0x28cc8c 000a
0x28cc8d 0000
0x28cc8e 0000
0x28cc8f 0000



このプログラムでは、4byteのint変数に整数値をセットしたとき、その値が内部的にどのように格納されているかを確認することができます。今回はintelのCPU上でコンパイルしたのですが、若い番地のアドレスに下位ビットの情報が入っていることがわかりました。このようなメモリ確保の方法を、小さい番地から使っているのでリトルエンディアン(little endian)と呼びます。

プログラムを実行しているCPUによっては、以下の出力になる場合もあります。
これは、データを大きい番地から使っているのでビッグエンディアン(big endian)と呼びます。

data2(cdata) --
0x28cc8c 0000
0x28cc8d 0000
0x28cc8e 0000
0x28cc8f 000a





2番目の例です
この例は、セットされたデータを複数の方法でアクセスする事ができるというサンプルです。このような書き方をすることは多くないですが、共用体の各メンバが指す先頭アドレスが同一だという事がわかればOKです。

#include <stdio.h>
#include <string.h>
 
struct DETAIL {
        char c0;
        char c1;
        char c2;
        char c3;
};
 
union DATA {
        char str_array[4];
        struct DETAIL d;
};
 
 
int main()
{
        union DATA c;
 
 
        strcpy( c.str_array, "abc" );
 
        printf( "\ndata2(idata) --\n" );
        printf( "%p %s\n", &c.str_array, c.str_array );
 
        printf( "\nc(cdata) --\n" );
        printf( "%p %c\n", &c.d.c0, c.d.c0 );
        printf( "%p %c\n", &c.d.c1, c.d.c1 );
        printf( "%p %c\n", &c.d.c2, c.d.c2 );
        printf( "%p %c\n", &c.d.c3, c.d.c3 );
 
        return 0;
}



実行結果

data2(idata) --
0x28cc8c abc
 
c(cdata) --
0x28cc8c a
0x28cc8d b
0x28cc8e c
0x28cc8f




次の例は、unionが実際にlinux等のOSで使用されている例です(説明しやすいように一部改変してあります)
ちょっと難しいので、よく意味が分からない方は読み飛ばしていただいても結構です。

以下のファイルは/usr/include/ext2fs/ext2fs.hから抜粋したものです。

/*
 * Structure of an inode on the disk
 */
struct ext2_inode {
    unsigned short int   i_mode;     /* File mode */
    unsigned short int   i_uid;      /* Low 16 bits of Owner Uid */
    unsigned int         i_size;     /* Size in bytes */
    unsigned int         i_atime;    /* Access time */
    unsigned int         i_ctime;    /* Inode change time */
    unsigned int         i_mtime;    /* Modification time */
    unsigned int         i_dtime;    /* Deletion Time */
    unsigned short int   i_gid;      /* Low 16 bits of Group Id */
    unsigned short int   i_links_count;  /* Links count */
    unsigned int         i_blocks;   /* Blocks count */
    unsigned int         i_flags;    /* File flags */
    union {
        struct {
            unsigned int         l_i_version; /* was l_i_reserved1 */
        } linux1;
        struct {
            unsigned int        h_i_translator;
        } hurd1;
    } osd1;             /* OS dependent 1 */
    unsigned int         i_block[EXT2_N_BLOCKS];/* Pointers to blocks */
    unsigned int         i_generation;   /* File version (for NFS) */
    unsigned int         i_file_acl; /* File ACL */
    unsigned int         i_dir_acl;  /* Directory ACL */
    unsigned int         i_faddr;    /* Fragment address */
    union {
        struct {
            unsigned short int   l_i_blocks_hi;
            unsigned short int   l_i_file_acl_high;
            unsigned short int   l_i_uid_high;   /* these 2 fields    */
            unsigned short int   l_i_gid_high;   /* were reserved2[0] */
            unsigned int         l_i_reserved2;
        } linux2;
        struct {
            unsigned char        h_i_frag;   /* Fragment number */
            unsigned char        h_i_fsize;  /* Fragment size */
            unsigned short int   h_i_mode_high;
            unsigned short int   h_i_uid_high;
            unsigned short int   h_i_gid_high;
            unsigned int         h_i_author;
        } hurd2;
    } osd2;             /* OS dependent 2 */
};



大きなくくりでみると、structの中に2つunionがあります。

これはHDDのファイルをOSが内部管理するための構造体です。
この構造体はlinuxやhurdという複数のOSが利用しており、両OSでデータ構造は、ほぼ同じなのですが一部OSによって依存(dependent)している部分があります。このOS依存の部分をunionとして定義することで、同一の構造体を利用しつつOS依存でフォーマットが異なる部分を吸収しています。

関連記事

コメントを残す

メールアドレスが公開されることはありません。