第11章 一心同体

 変数はメモリ上にあります。変数が違えば普通はそのアドレスは違うものですが、もし同じようにすることができるとしたらどうでしょうか? 今回はそういうお話です。


 では、今回の要点です。


 では、いってみましょう。


 int 型の変数があるとします。その内容は4バイトですが、これを1バイトずつ操作してみたいと思います。どうすればいいでしょうか?

 例えば、次のようにできると思います。

int a;

((unsigned char*)&a)[2] = 0;

 先ず、a のアドレスをとり、それを unsigned char* でキャストしています。こうすれば、a をあたかも char 型の配列であるかのように扱うことができるようになりますね。

 1ヶ所でしか使わないのであればこれで構いません。ですが、数ヶ所で使う場合はちょっと面倒です。なので、一旦アドレスを unsigned char 型へのポインタに入れておきましょう

int a;
unsigned char * const p = (char*)&a;

p[2] = 0;

 p の変更は行わないので、p は定数にしておきました(第1部第42章参照)。

 ここでだけでしかこういうことをしないのであれば、このままで構いません。ですが、いろんな場所でこういうことをするとすればこれでは不便です。

 考えられる方法として、クラスを使うというのがあります。

class CIntInByte
{
public:
    int i;

    unsigned char& InByte(int index);
};

inline unsigned char& CIntInByte::InByte(int index)
{
    return ((unsigned char*)&i)[index];
}

CIntInByte a;

a.InByte(2) = 0;

 このような数を扱うときに CIntInByte クラスを使うようにすれば、問題はなさそうです。


 しかし、こんなことをしなくても、実はもっと簡単にできるのです。

union UIntInByte
{
    int           i;
    unsigned char c[4];
};

UIntInByte a;

a.c[2] = 0;

 何やら、構造体に似た変な物を作ってますね。メンバ変数と思わしき i と c があります。

 そして、メンバ c を使っているだけです。一体どこで上と同じことをしているのでしょうか?

 実は、i と c はメモリ上の同じ位置に存在します。よって、c を書き換えれば i も、i を書き換えれば c も書き換えられます。そして、c を使えば i を1バイト単位で扱うことができるというわけです。

 構造体と似ているこれを共用体と呼びます。メモリを「共用」しているわけですね。

 共用体のメンバは構造体と違い、各メンバの先頭アドレスが同じになります。言い換えれば、同じメモリ領域に2つ以上の名前を付けているのです。

 変数の名前というのは人間がプログラムを組む上で分かりやすいようにつけているだけのことです。実際、実行ファイルになってしまうともう変数の名前は残っていません。

 となると、分かりやすいようにするために、同じメモリ領域に別の名前を与えることができてもいいわけです。それが共用体です。

 では、実際に共用体を使ってみましょう。

プログラム実行結果
// Union1.cpp
#include <stdio.h>

union UIntInByte
{
    int           i;
    unsigned char c[4];
};

// アドレスの表示
void DispAdrs(const UIntInByte& a);

// 値の表示
void DispVals(const UIntInByte& a);

int main()
{
    UIntInByte a;

    DispAdrs(a);

    a.i = 0;
    DispVals(a);

    a.c[2] = 0x23;
    DispVals(a);

    a.i = 0x016D7463;
    DispVals(a);

    return 0;
}

#define ELEM(array)  (sizeof (array) / sizeof *(array))

// アドレスの表示
// %p でアドレスを16進8桁で表示します
void DispAdrs(const UIntInByte& a)
{
    int        i;

    printf("&a.i    : 0x%p\n", &a.i);
    for(i = 0; i < ELEM(a.c); i++)
        printf("&a.c[%d] : 0x%p\n", i, &a.c[i]);

    putchar('\n');
}

// 値の表示
// %08X で16進数の最低8文字で、
// %02X で16進数の最低2文字で表示します
void DispVals(const UIntInByte& a)
{
    int        i;

    printf("a.i     : 0x%08X\n", a.i);
    for(i = 0; i < ELEM(a.c); i++)
        printf("a.c[%d]  : 0x%02X\n", i, a.c[i]);

    putchar('\n');
}
&a.i    : 0x0065FDF4
&a.c[0] : 0x0065FDF4
&a.c[1] : 0x0065FDF5
&a.c[2] : 0x0065FDF6
&a.c[3] : 0x0065FDF7

a.i     : 0x00000000
a.c[0]  : 0x00
a.c[1]  : 0x00
a.c[2]  : 0x00
a.c[3]  : 0x00

a.i     : 0x00230000
a.c[0]  : 0x00
a.c[1]  : 0x00
a.c[2]  : 0x23
a.c[3]  : 0x00

a.i     : 0x016D7463
a.c[0]  : 0x63
a.c[1]  : 0x74
a.c[2]  : 0x6D
a.c[3]  : 0x01

 先ずはアドレスの確認です。a.i のアドレスは 0x0065FDF4 です。では a.c[0] のアドレスは...ちゃんと同じ 0x0065FDF4 になっていますね。

 a.c[1] 以降も 0x0065FDF5, 0x0065FDF6, 0x0065FDF7 と、1バイト単位できれいに並んでいます。つまり、a.c[0] は a.i の0バイト目、a.c[1] は a.i の1バイト目、a.c[2] は a.i の2バイト目、a.c[3] は a.i の3バイト目になっているはずです。

 では、そのことを確かめてみましょう。先ずは a.i に0を代入してみました。a.i を表示すると0になるのは当たり前ですね。で、a.c[0] 〜 a.c[3] はどうなるでしょうか? きちんと0になっているようです。

 そして、その上で a.c[2] に 0x23 を代入しました。a.c[2] が 0x23 、他の a.c の値が0であることは問題ないのですが、a.i はどうなっているでしょうか? 0x00230000 になっているみたいですね。期待通りの動作になっています。(納得のいかない人は第1部第55章の「エンディアンネス」を参照。)

 そして、またその上で a.i に 0x016D7463 を代入しました。すると、a.c[0] は 0x63 に、a.c[1] は1 0x74 に、a.c[2] は 0x6D に、a.c[3] は 0x01 になっています。これも、期待通りの動作になっています。

 このように、共用体を使えば同じメモリ領域に違う名前を付けて、さらに違う型で扱うことができるようになるのです。


 それでは、今回の話の要点です。


 次回も構造体関係のお話です。それでは。


第10章 続・ポインタ天国 | 第12章 名無し

Last update was done on 2000.8.6

この講座の著作権はロベールが保有しています