第11回 プログラミングについて 『構造体のお話』
『構造体のお話』
今回は構造体のお話をしましょう。C言語では構造体、COBOLではレコードなどと呼ばれ、複数のデータが集合化された1つのかたまりのことです。C言語では構造体は自分で作れる新しい変数の型(本当に型として使用するには typedef 文を使用しますが)として扱うことができます。
またFORTRANの話になりますが、規格上でFORTRANが構造体をサポートするようになったのは、FORTRAN90になってからです。それまでも実際には各コンパイラには拡張機能としてサポートはされていましたが、以前まだ構造体が使用できないバージョンのFORTRANを使っていた頃は配列をいくつも作って1つ1つ操作したものです。その頃の私は構造体などという概念も知らなかったので別に不自由には感じなかったのですが、コンパイラのバージョンが上がって使えるようになると、それまでの苦労が馬鹿らしく思えたものです。しかし、構造体が使えないFORTRANだからといって、ただ指をくわえていたわけではありません。工夫のしかたで構造体をつくることができるのです。
VAX FORTRANの例ですが、
CHARACTER XY*8
CALL PUT_REAL4(%REF(XY(1)),10.0)
CALL PUT_REAL4(%REF(XY(4)),20.0)
:
:
SUBROUTINE PUT_REAL4(F,V)
REAL*4 F,V
F=V
RETURN
END
とするのです。これは2つ実数値を保管する8バイトの文字型の変数を作って、その先頭の4バイトに 10.0 を、次の4バイトに 20.0 を代入するものです。 %REF 関数はVAX FORTRANの組み込み関数で、引数にアドレスを渡すものです。FORTRANは基本的には全てアドレス渡しなのですが、VAX FORTRANの文字型の変数や定数は次のようにディスクリプタの形式の構造になっていて、通常ではこのディスクリプタのアドレスを渡すところを実際の値のアドレスを渡すようにします。
ディスクリプタ:
+--+--+----+
|TY|LN|ADDR|
+--+--+----+
↓
+------------+
|STRING |
+------------+
TY タイプ
LN 文字の長さ
ADDR 文字列のアドレス
STRING 文字列
実際には %REF を使わないで直接ディスクリプタのアドレスを渡しても代入できるようにアッセンブラで書いていましが... このように文字型の変数に整数値や実数値、もちろん文字列を代入してひとかたまりのレコードを作っていたのですが要素を直接アクセスできないので、やはりつらいものがあります。
さて、話をC言語に戻しましょう。C言語ではこのひとかたまりを struct 文を使って簡単に表現することができます。
struct xy_strct{
float x,y;
};
これは xy_strct という構造体は2つ実数値 x と y を各要素に持つかたまりと宣言されます。この状態では実際に変数は作成されません。
struct xy_strct xy;
として初めて変数が作成されます。宣言と定義を同時に行なうのであれば、
struct xy_strct{
float x,y;
}xy;
と記述します。構造体の要素の中には構造体が入っていても、ポインタが入っていても構いませんが注意しておかなければならないことがあります。要素の順序です。
struct data_strct{
char a;
short b;
char c;
};
と宣言すると、この構造体のサイズは何バイトになるでしょうか。サイズはコンピュータのアーキテクチュアやOSに依存しますが、PC98/DOS、HP/UNIX、DEC Alpha/UNIXでは6バイトになりました。char型は1バイト、 short型は2バイトで合計4バイトのはずですが、結果は違ったものになってしまうのです。これは変数のアラインメントが原因なのです。実際には a の後に1バイト、b の後に1バイトの空きができてしまっているのです。この空きは全く無駄なメモリーの消費となってしまいます。そこで、
struct data_strct{
char a;
char c;
short b;
};
と順序を変えると結果は4バイトになり、無駄な空きがなくなります。
このように変数の順序や、コンピュータの違いでサイズが変わることがありますので、違ったコンピュータに移植をする可能性のあるプログラムを書くときには、変数や構造体のサイズを固定して記述してはいけません。C言語には sizeof というコンパイル時に処理される便利な関数がありますので、これを使用します。
struct data_strct{
char a;
int b;
long c;
}data;
char tmp[8];
memcpy(tmp,data,8);
などと記述すると、確かにPC98/DOSでは、正常に data の内容が tmp にコピーされますが、HP/UNIXでは32ビットマシンなので int型は4バイト、long型も4バイトとなり、DEC Alpha/UNIXは64ビットマシンなので int型は4バイト、long型は8バイトとなりサイズを固定することは危険です。
struct data_strct{
char a;
int b;
long c;
}data;
char tmp[sizeof(struct data_strct)];
memcpy(tmp,data,sizeof(struct data_strct));
として、サイズの計算はコンパイラにまかせるようにします。移植をしないプログラムであっても、サイズを固定しない方が安全です。構造体の要素を変更したり、順序を変更するたびにサイズの設定を変更しなければならないからで、思わぬバグの原因となってしまいますので注意して下さい。
構造体を新しい型にするには typedef 文を使用します。
struct xy_strct{
float x,y;
};
typedef struct xy_strct XY;
とすると、構造体 xy_strct は新しく XY型として宣言することができます。XYは型なので、
XY xy;
として XY型の xy 変数を作ることができます。typedef文は便利なのですがこれも注意しないと分りずらいプログラムになってしまうことがあります。XYを typedef文で別の型に宣言することもでき、#define 文を使って #define XY struct xy_strct などともできますので、XYの構造がどうなっているのかを調べるのに宣言されている部分をさがさなければならなくなって、スマートに表現できる代わりに難解になる可能性のあることを覚えておいて下さい。
構造体の要素は、上の XY 型 xy のものは、
xy.x
xy.y
とドットを使って表現します。
XY *pxy;
というポインタのときには、
(*pxy).x
(*pxy).y
または、
pxy->x
pxy->y
とします。
構造体に似ていますが、共用体というものがあります。これは複数の要素が同じアドレスを持っているような構造を表現するもので、C言語では union文で宣言します。
union ic_union{
int i;
char c[sizeof(int)];
};
と宣言すると、int型の要素 i と char型の配列の要素 c のアドレスが同じである共用体 ic_union が宣言されます。上の例では i のバイト数と、c[]のバイト数が同じなので int型と同じサイズになりますが、違うときには要素の中の最大のものに合わせられたサイズとなります。
union ic_union ic;
として実際に共用体を定義します。共用体の要素のアクセスも構造体と同じで、
ic.i
ic.c[0]
などと表現します。
#include <stdio.h>
main()
{
union ic_union{
int i;
char c[sizeof(int)];
};
union ic_union ic;
int i;
ic.i=100;
for(i=0;i<sizeof(int);i++){
printf("%2.2x\n",ic.c[i]);
}
}
この例は、共用体 ic の int型の要素 i に100を代入し、char型の配列要素 c がどのようになるのかを表示するものです。結果は、
64
00
になります。要素のアドレスが同じですので当然 c の内容が変わる訳です。さらにこのシステム(PC98)は int型は2バイトで、下位のバイトが先頭になっているのでリトルエンディアンだということです。ちなみにHPのワークステーションでは、
00
00
00
64
となったので、int型は4バイトでビッグエンディアンということになります。
共用体はあまり使う頻度は高くはありません。私の場合は、せいぜい上記のような違ったシステムのバイナリファイルを変換するときに使うぐらいでしょうか。
それではまた次回。