C言語ワークショップ 第8回  構造体

 今回は構造体という高水準言語らしい概念を説明します。構造体は変数がちょっと高性能になったもので、非常によく使われる機能です。

●構造体変数

 構造体は構造化された変数(=構造体変数)であり、配列のように変数を複数並べて参照する点は同じですが、構造体はその並べる変数を全く違う型で組み合わせることが出来るものです。そして、それはあたかも一つの変数として扱えることが非常に重要かつ、構造体の便利な面であります。

 そしてなおかつ構造体変数はそれを一つの変数として扱えますので、構造体変数の配列を(構造体配列)組むことも可能であります。

●構造体変数の定義

 さて具体的に構造体の定義から見ていきましょう。
 構造体はまず使うに当たり定義をする必要がありますが、構造体を表す型はstruct型となります。いくつか方法がありますがまずは最もシンプルに構造体変数を定義してみましょう。

 struct st_name;

 このように基本形はやっぱり変数型+変数名で宣言します。
 が、これでは役不足です。このままだと実際に変数st_nameに何か操作を行おうとするとコンパイラはエラーを出します。(コンパイラによってはこの宣言だけでもNGかも・・・)

 この状態では構造体変数が実際にどのように構造化されているのか中身がよく分かりませんよね。
そのため次のようにその中身を具体的に示してあげる必要があります。

   struct { int a; } st_name;

 このように{ }でstruct型の中身(構造体の構造)を示してあげます。このブロックで囲まれた部分を構造体のテンプレートと呼びます。これで中身は定まりました。この構造体変数のst_nameはint aで構造化されていることになります。

 そして、構造体変数は中に並べる変数を全く違う型で組み合わせることが可能というわけですのでいろいろ並べてあげましょう。

 struct { int a; char b; long c; } st_name;

 このようにすれば構造体変数st_nameはテンプレート{ int a; char b; long c; }という3つの変数をメモリ上に順番に並べて一つのst_nameという構造体変数が準備されたわけです。

●構造体変数のメモリイメージとアラインメント

 さて先ほど宣言した

 struct { int a; char b; long c; } st_name;

 において、仮にint が2バイト、char が1バイト、longが4バイトとするとして、st_nameが2000h番地に割り当てられると次のようになります。
st_name
アドレス変数ブロック
2000h
2001h
int a2バイト
2002h
2003h
char b2バイト
2004h
2005h
2006h
2007h
long c;2バイト
2バイト

 このように、構造体はテンプレートのブロック内( { } )で宣言した順番に並べられます。

 しかし、よく見ると上の図ではchar型変数は1バイトですが、この図では2バイトが使用されています。ここでは表の書かれているブロックという値で示しています。

 これは処理系に依存しますが、まず最初の条件として、仮に16ビットでメモリアクセスされる処理系でにおいて、メモリアクセスが最小2バイト(16ビット)単位で行われる場合には、そのメモリ配置は必ず2バイトで割り切れるアドレス(2の倍数の番地)となることがあります。もし最小単位が4バイトならば4の倍数で位置で配置される可能性があるわけです。
 そして第2として、int型なら、2バイト、long型なら4バイトで配置されていく可能性があります。これは上の図で仮にchar型変数が無い場合でも、long型の始まりは2004hの4の倍数位置にくると言うことを意味します。

 これらことをメモリのアラインメントと呼びます。しかしながら実際には処理系によっては、これらの第1、2の法則通りになる保証はありません。これは実際に調べてみるのが良いでしょう。

 ここで重要なものは、このアラインメントが生じた場合に、図のような場合に、char型変数は1バイトを余分にパディングとして付加されることになり、構造体変数st_nameの大きさは8バイトで、またどこかにパディングとして不使用のエリアが出来る可能性があることを覚えておくと良いです。

 実際の処理系において、その構造体のサイズはサイズオブ演算子のsizeof(st_name)によって求めることが出来ます。Windows上など32ビットですと、最小のアライメントが4バイト単位になり、実際にsizeofを使用すると、12バイトが求まります。(Windows上ではint long共に4バイトで、charも4バイトにアラインメントされます)

   このように構造体変数は複数の変数を、そのトータルサイズの一変数として扱うことが可能なわけです。

●構造体変数の代入演算

 さて構造体に代入処理をしてみましょう。

 struct { int a; char b; long c; } st_name_a , st_name_b;

 今度は2つの構造体変数st_name_aとst_name_bを用意しました。この変数の代入は

 st_name_a=st_name_b;

  となります。何のことはありませんね。先ほどサイズを求めましたがこの8バイト変数はそのまま代入すれば、8バイトのデータが代入されるわけです。ただし、基本的には両辺が同じ構造体でなければなりません。そのため簡単な例で

 struct { int a; char b; long c; } st_name_a;
 struct { int a; char b; long c; } st_name_b;

 だと、実際には同じサイズですが、st_name_aとst_name_bは別物扱いにされてます。(同じにする方法は後ほど解説)

 また、構造体変数も当然普通の変数のように初期化出来ます。

 struct { int a; char b; long c; } st_name_a = { 1 , 2 , 3 } , st_name_b = { 0 };

 どんどん長くなりますが、このように構造体名の後に={ }で要素を並べてを書くことで初期化でき、st_name_bの初期化のように要素が1つのみならば、最初の要素(ここではint a)が0初期化されます。(ちなみにVCではそれ以外の要素も0初期化されますので実際にはすべてが0初期化されます。)

●構造体の識別名

 さて、先ほど

 struct { int a; char b; long c; } st_name_a;
 struct { int a; char b; long c; } st_name_b;

 と構造体を宣言すると、2つは別物扱いになるというお話をしました。
 よく見てみますとテンプレート部分は同じにしたいと思ったわけですから当然同じですよね。じゃこのテンプレートに名前を付けちゃえば同じ物として扱えるというわけで、早速名前を付けてみましょう。その方法は

 struct tagname{ int a; char b; long c; } st_name;

 と言った感じでテンプレートのブロックの前に名前を付けてあげます。この名前をタグ名と呼びます。 そしてst_name_a、st_name_bを同じ物にするために

 struct tagname{ int a; char b; long c; } st_name_a;
 struct tagname{ int a; char b; long c; } st_name_b;

 っと宣言したいところですが(笑、これではタグ名のtagnameを2回つけてしまってますね。もしこの2つのブロック内の要素が同じでなければタグ名を付けた意味が無いですし、2度もタグを定義する必要はありませんよね。ですから

 struct tagname{ int a; char b; long c; } st_name_a;
 struct tagname st_name_b;

若しくは

 tagname st_name_c;

 という感じでstruct + タグ名かタグ名のみで変数を宣言できます。そしてこの3つの変数はすべて同じ構造の構造体であり、代入を自由に行うことが出来ます。

 また先にタグ名だけをつけたい場合には

 struct tagname{ int a; char b; long c; };

 struct tagname st_name_a,st_name_b;
 tagname st_name_c,st_name_d;

 といったように、先にタグ名だけの構造体を宣言することが可能です。

●構造体のデータ型名の定義

 構造体を使うにあたりよく使われるものやプログラム全体で決まった物を用いる場合に、プログラムの最初でデータの型を先に定義しておくことが出来ます。 構造体に限らず型定義子のtypedefというを指定子使用して型の別名をつけることが可能です。その方法は一般的に

 typedef  型名  エイリアス名;

 という形になります。たとえばunsigned int変数にエイリアス名をつけると

 typedef  unsigned int  uINT;

 でこのuINTはintのエイリアスとなり以降、intを使わなくても

 uINT udata;

 のようにエイリアスが同意の型名となります。

 これを構造体に用いると型名部分(下線の部分)が構造体の型となり

 typedef struct tagname{ int member1; char member2; } ALIASNAME;

 とすることで以降、ALIASNAMEが先ほど説明したuINT型と同じように変数自体の型名となりますので、

 ALIASNAME st_name;
 若しくは
 struct tagname st_name;
 tagname st_name;

 のように出来ます。

 ただし
 struct ALIASNAME st_name;
 はできません。ALIASNAMEはエイリアスでありタグ名ではないからです。

 また、ここでは、見やすくするために1行で構造体を定義していますが、実際のプログラムにおいてはテンプレートを書きやすくするため、1行ではなく

 typedef struct tagname{
  int member1;
  char member2;
  char member3;
 }ALIASNAME;

 のように型定義を書くことが一般的で、また構造体の場合宣言でも

 struct tagname{
  int member1;
  char member2;
  char member3;
 } st_name;

 といったように複数行で定義されることが一般的です。
 この両者はぱっと見て同じ形ですが、最後のALIASNAMEはエイリアス名、 st_nameは構造体変数であり、前者はtypedefされる物ですので、別物であることに注意が必要かと思います。

●構造体変数のメンバー参照

 さて、普通の変数としての代入等、構造体は1つの変数として扱うことが出来たわけですが、その中身である構造化された変数にも個別にアクセス出来ると大変便利です。
 そこで、構造化された変数それぞれを構造体のメンバーと呼びそのメンバーを参照することが出来ます。

 struct tagname{
  int member1;
  char member2;
 } st_name;

 st_name.member1 = 1;
 st_name.member2 = 2;

 このように「.」(ドット)の後にそのメンバー名を示すと、そのメンバー変数にアクセスすることが出来ます。そして、このドットを構造体のメンバー参照演算子と呼びます。これによりst_nameという変数名の構造体に対しドットを用いてメンバー参照演算を行い、そのメンバーを示すメンバー名はmember1やmember2ということになるわけです。

●構造体変数の配列

 さて、構造体変数はそれ自体が任意長(サイズの自由な)の一つの変数であり、これを配列化して扱うことも当然可能です。その方法は

 typedef struct tagname{
  int member1;
  char member2;
 } ALIASNAME;

 struct tagname st_name[10];
 若しくは
 ALIASNAME st_name[10];

 のように変数名の後ろに通常通り[]で配列定義をすることで、配列を組むことが出来ます。

 またそのメンバーへのアクセスも

 st_name[0].member1=0;

 といった形で構造体配列のメンバー参照をすることができます。

●構造体変数のポインタ

 さて、構造体も可変長の1変数でありますから、当然ポインタがありますよね。

 typedef struct tagname{
  int member1;
  char member2;
 } ALIASNAME;

 ALIASNAME st_name;

 &st_name;

 このように構造体変数の割り当てられたアドレスを求めるには通常通り、&演算子(アドレス演算子)を使います。

 また、構造体のポインタ変数は*(ポインタ宣言)を用いて

 struct tagname *p_st_name;
 または
 ALIASNAME *p_st_name;

 として、構造体のポインタ変数を宣言できます。

 その代入も普通と変わりなく

 p_st_name = &st_name;

 というように代入します。

 先に構造体へのポインタ変数もtypedefしてしまうことが出来ます。

 typedef struct tagname{
  int member1;
  char member2;
 } ALIASNAME, *p_ALIASNAME;

 ALIASNAME  st_name;
 p_ALIASNAME p_st_name;

 このようのp_ALIASNAMEはtypedefにおいて *p_ALIASNAMEの形ですでにポインタ変数を表すエイリアス名になっています。そのため実際にエイリアスを用いて変数を宣言する場合にはポインタ型の*はすでに含まれているので

 p_ALIASNAME p_st_name;

 という形でポインタ変数の宣言となります。

●構造体変数のポインタに対する間接参照とメンバーの参照

 ここまでくると当然ポインタの間接参照も出てきます。

 typedef struct tagname{
  int member1;
  char member2;
 } ALIASNAME;

 ALIASNAME st_name_a,st_name_b;
 ALIASNAME *p_st_name;

 p_st_name = &st_name_a;

 st_name_b = *p_st_name;

 このように、構造体へのポインタ変数p_st_nameの間接参照も間接参照演算子(*)を用いることでポインタの示す構造体を求めることが出来るわけです。

 どんどん奥深い物になりますが、今度はポインタの示す構造体のメンバーにアクセスしたい場合はどうでしょうか。今までの課程をふまえれば自然に見えてくると思いますが、ポインタの間接参照したものにメンバー参照すればいいわけで

 *p_st_name.a;

 としたいところですが、実はこれだとNGなのです。
 間接参照演算子(*)とメンバー参照演算子ですと優先順位が強いのはメンバー参照演算子なのです。ですので、これだとp_st_nameのメンバー参照(p_st_name.a)が先に行われ、これに対して間接参照する

 *(p_st_name.a);

 を行っていることになってしまいます。そこで

 (*p_st_name).a;

 というように()で先にポインタに対して間接参照を行ったものにメンバー参照する必要があります。

 さて、演算子の扱いや意味を理解する上では重要な表記ですが、実際使っていく場合にはいちいち括弧で括らなければならないのでちょっとスマートではないですよね。
 そこで、演算子には構造体ポインタの実体に対してメンバー参照を行う専用の演算子が用意されています。

 p_st_name->a;

このように->演算子の後に参照したいメンバーを指定することで簡単に書くことが出来ます。

 st_name.a;  で構造体のメンバー参照をする
 p_st_name->a; で構造体ポインタへ間接参照したものをメンバー参照する

となるわけです。