組み込みC/C++

C/C++リテラシー向上のためのページ

構造体変数の考察

先のエントリビットフィールドの使用方法の中でキャストをdefineする方法を紹介しました。

 

typedef struct BIT_TYPE1_{
    unsigned btCom0:1;
    unsigned btCom1:1;
	unsigned btCom2:1;
	unsigned btCom3:1;
	unsigned btDummy:4;	
} BIT_TYPE1;
#define DfBitType1(X) (*( (BIT_TYPE1*)(&X) ) )

 

このdefine、何をやっているかとても判りにくいので少し補足していこうと思います。ふつう通常の変数(int型とかunsigned char型とか)を構造体にするのであれば、次のようなキャストでいいんじゃないのか?と思ってしまいます。

 

unsigned char iCom1;
    
(BIT_TYPE1)iCom1.btCom0=1;
    

しかしこれはコンパイルエラーとなります。iCom1から直接BIT_TYPE1型にはキャストできません。これは構造体で宣言された構造体変数が特殊な型な為コンパイラ内部に型変換する関数(コンストラクタ)が用意されていないのです。その為に一度&で変数をアドレスにしてから(BIT_TYPE1*)でキャストして(アドレスからアドレスはどんな型でも型変換できます。void*の原理です)もう一度*で実体にするという面倒な事をしているのです。では構造体変数がどう特殊なのか見ていきたいと思います。

 

    typedef struct StData_{
		unsigned int x;
		unsigned int y;
		unsigned int z;
		unsigned int w;
	}StData;

	StData stData = {15, 21, 32, 5};
	
	printf("stData decimal=%d, %d, %d, %d\n", stData );

上のように初期値を与えた構造体変数、printf文でその中身を見てみたいと思います。構造体自体はint型が四つあるので全部で16byteの領域(32bit PCの場合)を使っています。printfの%dの数と引数の数が一致していませんが、結果を見てみましょう。

 

stData decimal=15, 21, 32, 5

 

stDataの構造体に与えた初期値がそのまま出ています。これはprintf文の裏ワザ的な使い方なんですが、まずはprintf文の動きから説明します。printf文で%dを書くと引数から4byte領域を引っ張ってきて符号付きの整数で値を吐き出します。この場合は一個の引数16byte(stDataが持っている領域)に対応する為に%dが4つ必要になっています。出力は4byteづつ区切った値が出てきているのです。intをshortなどにすれば判りますが4byteでただ区切っている値です。ここで構造体変数の話しに戻りますが、内部的には構造体で定義するとべた書きで数字が並んでいるだけなのです。この場合は16byteの型を新規に作ったと考えるとよいかと思います。この特殊性が先の単純なキャストを不可能にしているのです。また他の演算を直に構造体変数に作用させる事はできません。唯一許された演算が 構造体変数 = 構造体変数; の演算です。この代入演算は構造体のメンバ一つ一つをコピーしていたのではなく確保した領域の値をまるごとコピーしていたというわけだったのです。

 

さて、少し話しはそれますが、せっかくですので構造体のアドレスの記法と配列のアドレスの記法を対象的に見ていきましょう。より理解が深まるかと思います。

 

    StData stData = {15, 21, 32, 5};
	printf("stData address=%d, address2=%d,\n", &stData, &(stData.x) );
	printf("stData address=%d, address2=%d,\n", ( (&stData)+1), ( (&(stData.x) )+1) );

	unsigned char a [ ] = {1,2,3};
	printf("a [ ]= %d, %d, %d \n", a, &a, &a[0]);
	printf("a [ ]= %d, %d, %d, %d \n", (a+1), ( (&a)+1), *( (&a)+1), ( (&a[0])+1) );    

 

さて構造体は二種類、配列は三種類、先頭アドレスを参照する方法があります。上の例ではどちらも、最初のprintf文は同じアドレスが表示されます。ではアドレスのインクリメントはどういった働きをするでしょうか?

 

stData address=2685768, address2=2685768,
stData address=2685784, address2=2685772,
a= 2685756, 2685756, 2685756
a
= 2685757, 2685759, 2685759, 2685757

 

&stDataから( (&stData + 1) )の差分は16byte、&(stData.x)から( (&stData.x) +1)までの差分は4byteとなっています。前者は構造体の大きさごと、後者はメンバの大きさでインクリメントされています。この事からもStDataという型は16byteの新しい型である事が認識できるかと思います。おまけとして配列のアドレス比較もしてみました。配列では&aから( (&a) + 1)までの差分が配列全体の大きさになっており&aという書き方が特殊な型になります。なお配列の場合[ ]を外して変数名だけを書いた場合はアドレスになってしまうので構造体のようにまるごとコピーという技が使えません。

 

以上が構造体変数の考察でした、構造体変数が新しい型の箱だったとわかっていただければと思います。そういえば構造体配列というものもありますが、ここまで紹介した内容を理解していれば、どういったふるまいをするか判るかと思いますので考えてみてください。