コラム
2024/11/29
第87回 プログラミングについて『いまさらですがC言語の門を叩こう その10』〜 構造体 〜
これまでお話ししてきたことがC言語の基礎で、これだけ知っていれば(関数のライブラリについてはあまり説明してはいませんが)とりあえずプログラムを作成することができます。
しかしながらデータの表現に関するならば基本の型のみで表現するとなるとちょっと辛いと思います。単純なデータに対しての計算のみしか行なわないプログラムであれば、基本の型を使うだけでも十分ですが、実際にはこのようなことの方が少ないかもしれません。例えば住所録を扱うプログラムを作ろうとしたときには、名前、郵便番号、住所、電話番号などの項目が必要ですので、
#define MAX_DATA 100
#define MAX_NAME 21
#define MAX_POST 11
#define MAX_POST 81
#define MAX_TEL 21
char name[MAX_DATA][MAX_NAME];
char post[MAX_DATA][MAX_POST];
char addr[MAX_DATA][MAX_ADDR];
char tel [MAX_DATA][MAX_TEL ];
などとして、それぞれの項目を別々の配列に代入する形式で表現することも可能です。
このようにしてプログラムを作成することは勿論可能です。FORTRAN77以前のFORTRANでは基本の型の変数と配列しか使えないので、基本的にはこのような方法しか選択の余地はありませんでした。
プログラムを作ることができるとは言っても、やっぱり辛いことは確かです。できれば1人分のデータは1つになっていて欲しいと思うのが人情だと思います。私がFORTRANでプログラムを書いていた頃(VAX FORTRANの拡張機能にまだ構造体がサポートされていない頃)は、このような不都合を解消するために文字型の変数を構造体代りに使用したものでした。とにもかくにも文字変数のどこからどこまでが住所、どこからどこまでが郵便番号としてプログラムを作っていたのです。これも不便といえば不便なのですが、それぞれの項目の配列を別々に作成するよりも少しはましなプログラムだったと思います。
このような不便さを解決するために構造体というものが考え出されました。もっとも構造体というのはC言語に最初に採用されたものでも、そんなに新しい考えのものではありません。古くはプログラミング言語の元祖とも言えるCOBOLではレコードとして採用されていたものです(言ってしまえばFORTRANがいつまでも採用しなかっただけのことかも知れません)。
複数の項目を1つのデータの中に表現できるとすればプログラムもスッキリし、かなり明快に表現できるのです。上の例をC言語の構造体で表現すると、
struct data_strct{
char name[MAX_NAME];
char post[MAX_POST];
char addr[MAX_ADDR];
char tel [MAX_TEL ];
};
struct data_strct data[MAX_DATA];
となります。このようにすると1つのデータの中に4つの変数があることになります。
この構造体の中の変数をメンバーとかフィールドとか呼んでいるようです。ここで構造体の宣言の形式を説明しましょう。
struct タグ名 {
メンバー;
:
:
}構造体名, ... ;
の形式です。ここで、タグ名は構造体の種類を識別をするための名称で省略することもできます。構造体名はいわゆる変数名と同じもので、これも省略することができます。
ただタグ名と構造体名の両方を省略すると、コンパイラには叱られませんが、プログラムとしては何にも意味を持たないことになります。
struct {
int a,b,c;
}data;
としてタグ名を省略しても構造体 data は正しく作成することができますが、構造体の名前までも省略すると、
struct {
int a,b,c;
};
となり、この構造を利用する手だてがないのです。構造体名を省略してもタグ名を付けておけば、
struct data_strct{
int a,b,c;
};
struct data_strct data;
というようにタグ名を利用して構造体を定義することができるようになります。
構造体の型の宣言中でも構造体の型の宣言をすることもできます。
struct data_strct{
struct sub_strct{
int a,b,c;
}sub;
}data;
とすれば構造体 data の中に構造体 sub があり、sub のメンバーに int 型の a、b、cがあることになります。このときそれぞれのメンバーは、
data.sub.a
data.sub.b
data.sub.c
として参照することができます。
struct data_strct{
struct sub_strct{
int a,b,c;
};
}data;
として構造体の中の構造体宣言に構造体名を指定しない方法もあり、それぞれのメンバーは、
data.a
data.b
data.c
として参照できますが、この形式はマイクロソフトなどのコンパイラの拡張ですので他のシステムに移植する場合や、他のコンパイラでコンパイルするときにはエラーとなってしまいますので注意して下さい。
話しが前後してしまいましたが、構造体のメンバーを参照するときには、ドット(.)を使用します。構造体へのポインタの場合でも同じで、
struct data_strct{
int a,b,c;
}*data;
のときには、
(*data).a
(*data).b
(*data).c
という表現になります。ただし、C言語では構造体へのポインタは頻繁に使用するため、-> という演算子を使ってもっと端的に表現できるようになっています。上の例の場合は、
data->a
data->b
data->c
となります。これだけですと -> 演算子のありがたみはあまりないようですが、
struct data_strct{
struct sub_strct *sub;
}*data;
struct sub_strct{
int a,b,c;
};
のように、構造体のメンバーが構造体へのポインタであるようなときには、ドットを使ってメンバーを参照するには、
(*(*data).sub).a
(*(*data).sub).b
(*(*data).sub).c
となって理解しづらいのですが -> を使うと、
data->sub->a
data->sub->b
data->sub->c
となり、直感的に理解できるようになるのです。実際に複雑なプログラムを作ってみるとこの -> 演算子のありがたみがひしひしと身にしみるものです。試しに自分のプログラムの中を探してみたところ、
bl[i]->xy_data_top->next->next->y
という部分がありました。もしこれをドットの形式で表現すると、
(*(*(*(*tbl[i]).xy_data_top).next).next).y
となり書くのも大変ですが、読むときにはもっと苦労すると思います。
構造体のメンバーには自分自身の構造体の型へのポインタを宣言することができます。
最初の例を、構造体を配列形式ではなく自分自身の型へのポインタでリスト形式で表現するとすると、
struct data_strct{
char name[MAX_NAME];
char post[MAX_POST];
char addr[MAX_ADDR];
char tel [MAX_TEL ];
struct data_strct *next;
};
struct data_strct *data;
というように記述することができます。ここで、構造体 data_strct の中の
struct data_strct *next;
というメンバーが問題です。これはあくまでも data_strct 構造体へのポインタであり、data_strct 構造体そのものではありません。もし、
struct data_strct{
char name[MAX_NAME];
char post[MAX_POST];
char addr[MAX_ADDR];
char tel [MAX_TEL ];
struct data_strct next;
};
とするとコンパイラになんらかの理由で叱られてしまいます。
C言語でのプログラムでは自分自身へのポインタをメンバーに持つ構造体は頻繁に(ごく当たり前のように)使用されます。配列形式でデータを表現した場合には配列の要素数で扱えるデータの最大数が制限されることになるので、データ数の最大値が分かっているときには問題はないのですが、データ数の最小値も最大値も一意的に決定できないときには、もし配列形式で表現するとなると予想される最大値より大きい要素数にしなければなりません。そうなると、データ数の少ないときには無駄なメモリーを消費し、予想外にデータ数が多いときには正常に処理できなくなることになります。
動的にメモリーを確保するような記述をすることが困難なFORTRAN77以前の言語ではおおざっぱに要素数を大きくするしかありませんが、C言語では動的にメモリーを確保することがごく当たり前のような言語では配列形式で表現することは得策ではありません。
ちょっと余談になりますが、C言語では動的にメモリーを確保することが頻繁に行われます。C言語ではポインタを簡単に扱うことができるので動的に確保したメモリーであっても、そうでないものであっても殆ど同じような扱いができるのです。これは非常に便利で強力な機能なのですが、注意しなければならないことは、動的に確保したメモリーを使い終わったら解放してあげなければならないことです。Windowsではプログラムの終了と同時に動的に確保したメモリーはOSによって解放されますが、プログラムの実行中は確保されたままになっているのです。例えば、
int *a;
a=(int )malloc(500); / 動的に500バイトを確保しそのアドレス
を a に代入する */
a=NULL; /* a を初期化するので、確保したメモリーのアドレ
スは永遠に分からなくなってしまう */
としてしまうと、動的に確保した500バイトのメモリーはプログラムの終了までそのまま無駄に居残ってしまうのです。このようなことをメモリーリークと一般に呼ばれています。よっぽど注意しないとある程度の大きさのプログラムを作ったときにはメモリーリークが発生してしまいますので注意が必要です。
話しを戻しましょう。
構造体についてちょっとだけ注意しておくことがあります。それは、構造体のメンバーの順序です。
struct data_strct{
char a;
int b;
char c;
int d;
};
いまこのような構造体があったとき、int 型が4バイトならばこの構造体は10バイトのメモリしか使用しません。この構造体のサイズを次のプログラムで調べてみると、
main()
{
printf("%d\n",sizeof(struct data_strct));
}
16と表示されるのです。この値はコンピュータ、OS、コンパイラ、コンパイラのオプションなどで違うこともありますが、希望通りの10と表示されることは少ないと
思います。
このように10バイトのはずがそれ以上のサイズになってしまうのは、計算機のアーキテクチュアによって変数の先頭のアドレスを置く位置が決められているからなのです。試しに、
struct data_strct{
char a;
short b;
char c;
short d;
};
という構造体で調べてみると、8になりました。ということは私の現在の環境では、int型は4の倍数の位置、short 型は2の倍数の位置に配置されると考えられます。
それでは一体この数値の差はどのようになっているのでしょうか再び次の構造体で考えてみましょう。
struct data_strct{
char a;
int b;
char c;
int d;
};
この構造体の内容を図にしてみると、
メンバー a ■□□□
メンバー b ■■■■
メンバー c ■□□□
メンバー d ■■■■
となります。ここで塗りつぶされた四角は使用するメモリで、そうでないものは何も使用されないものです。要するに、16バイトの中の6バイトが穴が空いたようになっている訳です。実際にプログラムを作るとこのような穴がどうしてもできてしまうことがあるのですが、これを少しでも少なくするにはメンバーの順序を変えてみることです。
struct data_strct{
char a;
char c;
int b;
int d;
};
としてみると、サイズが12になりますので、次のようになっていると考えられます。
メンバー a,c ■■□□
メンバー b ■■■■
メンバー d ■■■■
色々と工夫してみてもこのメンバーでは12バイト未満になりませんでしたが、4バイトだけ少なくなりました。しかし、たかが4バイトと馬鹿にしてはいけません。もしこの構造体で一万個の要素の配列を作ると、4万バイトの差があることになるのです。
いつでもサイズを優先させなければならないという義務はありませんので、プログラムが読みづらくならない程度にメンバーの順序に気を付けることが必要です。
それではまた次回。
しかしながらデータの表現に関するならば基本の型のみで表現するとなるとちょっと辛いと思います。単純なデータに対しての計算のみしか行なわないプログラムであれば、基本の型を使うだけでも十分ですが、実際にはこのようなことの方が少ないかもしれません。例えば住所録を扱うプログラムを作ろうとしたときには、名前、郵便番号、住所、電話番号などの項目が必要ですので、
#define MAX_DATA 100
#define MAX_NAME 21
#define MAX_POST 11
#define MAX_POST 81
#define MAX_TEL 21
char name[MAX_DATA][MAX_NAME];
char post[MAX_DATA][MAX_POST];
char addr[MAX_DATA][MAX_ADDR];
char tel [MAX_DATA][MAX_TEL ];
などとして、それぞれの項目を別々の配列に代入する形式で表現することも可能です。
このようにしてプログラムを作成することは勿論可能です。FORTRAN77以前のFORTRANでは基本の型の変数と配列しか使えないので、基本的にはこのような方法しか選択の余地はありませんでした。
プログラムを作ることができるとは言っても、やっぱり辛いことは確かです。できれば1人分のデータは1つになっていて欲しいと思うのが人情だと思います。私がFORTRANでプログラムを書いていた頃(VAX FORTRANの拡張機能にまだ構造体がサポートされていない頃)は、このような不都合を解消するために文字型の変数を構造体代りに使用したものでした。とにもかくにも文字変数のどこからどこまでが住所、どこからどこまでが郵便番号としてプログラムを作っていたのです。これも不便といえば不便なのですが、それぞれの項目の配列を別々に作成するよりも少しはましなプログラムだったと思います。
このような不便さを解決するために構造体というものが考え出されました。もっとも構造体というのはC言語に最初に採用されたものでも、そんなに新しい考えのものではありません。古くはプログラミング言語の元祖とも言えるCOBOLではレコードとして採用されていたものです(言ってしまえばFORTRANがいつまでも採用しなかっただけのことかも知れません)。
複数の項目を1つのデータの中に表現できるとすればプログラムもスッキリし、かなり明快に表現できるのです。上の例をC言語の構造体で表現すると、
struct data_strct{
char name[MAX_NAME];
char post[MAX_POST];
char addr[MAX_ADDR];
char tel [MAX_TEL ];
};
struct data_strct data[MAX_DATA];
となります。このようにすると1つのデータの中に4つの変数があることになります。
この構造体の中の変数をメンバーとかフィールドとか呼んでいるようです。ここで構造体の宣言の形式を説明しましょう。
struct タグ名 {
メンバー;
:
:
}構造体名, ... ;
の形式です。ここで、タグ名は構造体の種類を識別をするための名称で省略することもできます。構造体名はいわゆる変数名と同じもので、これも省略することができます。
ただタグ名と構造体名の両方を省略すると、コンパイラには叱られませんが、プログラムとしては何にも意味を持たないことになります。
struct {
int a,b,c;
}data;
としてタグ名を省略しても構造体 data は正しく作成することができますが、構造体の名前までも省略すると、
struct {
int a,b,c;
};
となり、この構造を利用する手だてがないのです。構造体名を省略してもタグ名を付けておけば、
struct data_strct{
int a,b,c;
};
struct data_strct data;
というようにタグ名を利用して構造体を定義することができるようになります。
構造体の型の宣言中でも構造体の型の宣言をすることもできます。
struct data_strct{
struct sub_strct{
int a,b,c;
}sub;
}data;
とすれば構造体 data の中に構造体 sub があり、sub のメンバーに int 型の a、b、cがあることになります。このときそれぞれのメンバーは、
data.sub.a
data.sub.b
data.sub.c
として参照することができます。
struct data_strct{
struct sub_strct{
int a,b,c;
};
}data;
として構造体の中の構造体宣言に構造体名を指定しない方法もあり、それぞれのメンバーは、
data.a
data.b
data.c
として参照できますが、この形式はマイクロソフトなどのコンパイラの拡張ですので他のシステムに移植する場合や、他のコンパイラでコンパイルするときにはエラーとなってしまいますので注意して下さい。
話しが前後してしまいましたが、構造体のメンバーを参照するときには、ドット(.)を使用します。構造体へのポインタの場合でも同じで、
struct data_strct{
int a,b,c;
}*data;
のときには、
(*data).a
(*data).b
(*data).c
という表現になります。ただし、C言語では構造体へのポインタは頻繁に使用するため、-> という演算子を使ってもっと端的に表現できるようになっています。上の例の場合は、
data->a
data->b
data->c
となります。これだけですと -> 演算子のありがたみはあまりないようですが、
struct data_strct{
struct sub_strct *sub;
}*data;
struct sub_strct{
int a,b,c;
};
のように、構造体のメンバーが構造体へのポインタであるようなときには、ドットを使ってメンバーを参照するには、
(*(*data).sub).a
(*(*data).sub).b
(*(*data).sub).c
となって理解しづらいのですが -> を使うと、
data->sub->a
data->sub->b
data->sub->c
となり、直感的に理解できるようになるのです。実際に複雑なプログラムを作ってみるとこの -> 演算子のありがたみがひしひしと身にしみるものです。試しに自分のプログラムの中を探してみたところ、
bl[i]->xy_data_top->next->next->y
という部分がありました。もしこれをドットの形式で表現すると、
(*(*(*(*tbl[i]).xy_data_top).next).next).y
となり書くのも大変ですが、読むときにはもっと苦労すると思います。
構造体のメンバーには自分自身の構造体の型へのポインタを宣言することができます。
最初の例を、構造体を配列形式ではなく自分自身の型へのポインタでリスト形式で表現するとすると、
struct data_strct{
char name[MAX_NAME];
char post[MAX_POST];
char addr[MAX_ADDR];
char tel [MAX_TEL ];
struct data_strct *next;
};
struct data_strct *data;
というように記述することができます。ここで、構造体 data_strct の中の
struct data_strct *next;
というメンバーが問題です。これはあくまでも data_strct 構造体へのポインタであり、data_strct 構造体そのものではありません。もし、
struct data_strct{
char name[MAX_NAME];
char post[MAX_POST];
char addr[MAX_ADDR];
char tel [MAX_TEL ];
struct data_strct next;
};
とするとコンパイラになんらかの理由で叱られてしまいます。
C言語でのプログラムでは自分自身へのポインタをメンバーに持つ構造体は頻繁に(ごく当たり前のように)使用されます。配列形式でデータを表現した場合には配列の要素数で扱えるデータの最大数が制限されることになるので、データ数の最大値が分かっているときには問題はないのですが、データ数の最小値も最大値も一意的に決定できないときには、もし配列形式で表現するとなると予想される最大値より大きい要素数にしなければなりません。そうなると、データ数の少ないときには無駄なメモリーを消費し、予想外にデータ数が多いときには正常に処理できなくなることになります。
動的にメモリーを確保するような記述をすることが困難なFORTRAN77以前の言語ではおおざっぱに要素数を大きくするしかありませんが、C言語では動的にメモリーを確保することがごく当たり前のような言語では配列形式で表現することは得策ではありません。
ちょっと余談になりますが、C言語では動的にメモリーを確保することが頻繁に行われます。C言語ではポインタを簡単に扱うことができるので動的に確保したメモリーであっても、そうでないものであっても殆ど同じような扱いができるのです。これは非常に便利で強力な機能なのですが、注意しなければならないことは、動的に確保したメモリーを使い終わったら解放してあげなければならないことです。Windowsではプログラムの終了と同時に動的に確保したメモリーはOSによって解放されますが、プログラムの実行中は確保されたままになっているのです。例えば、
int *a;
a=(int )malloc(500); / 動的に500バイトを確保しそのアドレス
を a に代入する */
a=NULL; /* a を初期化するので、確保したメモリーのアドレ
スは永遠に分からなくなってしまう */
としてしまうと、動的に確保した500バイトのメモリーはプログラムの終了までそのまま無駄に居残ってしまうのです。このようなことをメモリーリークと一般に呼ばれています。よっぽど注意しないとある程度の大きさのプログラムを作ったときにはメモリーリークが発生してしまいますので注意が必要です。
話しを戻しましょう。
構造体についてちょっとだけ注意しておくことがあります。それは、構造体のメンバーの順序です。
struct data_strct{
char a;
int b;
char c;
int d;
};
いまこのような構造体があったとき、int 型が4バイトならばこの構造体は10バイトのメモリしか使用しません。この構造体のサイズを次のプログラムで調べてみると、
main()
{
printf("%d\n",sizeof(struct data_strct));
}
16と表示されるのです。この値はコンピュータ、OS、コンパイラ、コンパイラのオプションなどで違うこともありますが、希望通りの10と表示されることは少ないと
思います。
このように10バイトのはずがそれ以上のサイズになってしまうのは、計算機のアーキテクチュアによって変数の先頭のアドレスを置く位置が決められているからなのです。試しに、
struct data_strct{
char a;
short b;
char c;
short d;
};
という構造体で調べてみると、8になりました。ということは私の現在の環境では、int型は4の倍数の位置、short 型は2の倍数の位置に配置されると考えられます。
それでは一体この数値の差はどのようになっているのでしょうか再び次の構造体で考えてみましょう。
struct data_strct{
char a;
int b;
char c;
int d;
};
この構造体の内容を図にしてみると、
メンバー a ■□□□
メンバー b ■■■■
メンバー c ■□□□
メンバー d ■■■■
となります。ここで塗りつぶされた四角は使用するメモリで、そうでないものは何も使用されないものです。要するに、16バイトの中の6バイトが穴が空いたようになっている訳です。実際にプログラムを作るとこのような穴がどうしてもできてしまうことがあるのですが、これを少しでも少なくするにはメンバーの順序を変えてみることです。
struct data_strct{
char a;
char c;
int b;
int d;
};
としてみると、サイズが12になりますので、次のようになっていると考えられます。
メンバー a,c ■■□□
メンバー b ■■■■
メンバー d ■■■■
色々と工夫してみてもこのメンバーでは12バイト未満になりませんでしたが、4バイトだけ少なくなりました。しかし、たかが4バイトと馬鹿にしてはいけません。もしこの構造体で一万個の要素の配列を作ると、4万バイトの差があることになるのです。
いつでもサイズを優先させなければならないという義務はありませんので、プログラムが読みづらくならない程度にメンバーの順序に気を付けることが必要です。
それではまた次回。