7.1 構造体
7.1.1 複数の型からなる構造
配列は、ある型の変数を複数集めた集合でした。このように、C言語の単純型を集めた集合的な要素を合成体と呼んでいます。この場で解説する構造体も、配列と同様に単純型を集めた合成体です。
配列は、指定した型を複数集めた合成体でしたが、構造体は異なる型の変数を一つにまとめた型のようなものです。これは、論理的につながりのある複数の情報を整理するために有効な、極めて重要な機能です。大規模なシステム開発になれば、構造体はなくてはならない存在です。
論理的につながりのある情報とは、例えばグラフィックスにおいて座標やサイズは重要な要素です。座標は x 座標と y 座標の 2 つの数値情報から成り立ちますし、サイズも幅と高さという 2 つの情報から成り立ちます。これらをばらばらに扱うこともできますが、近代プログラミングにおける設計思想からは反します。このように、人間から見てつながりのある情報はひとつに整理することが望まれるのです。そうすることで、プログラマはより確実に、スマートに、情報を関数やシステムの間で伝達させることができるようになります。
このとき、構造体が保有する各変数のことをメンバと呼びます。配列のように要素と呼んでも概念的に間違いではありませんが、あまりそのような表現が使われることはありません。
構造体の宣言は struct キーワードを用い、次のような書式を持ちます。
struct タグ名 { 型 メンバ名1; 型 メンバ名2; ... } 構造体変数リスト ;
タグ名にはこの構造体の名前を指定します。これは、構造体の変数を宣言する時に、構造体を参照するために利用されます。構造体の内部では型とメンバ名を指定して、構造体のメンバを宣言します。メンバ宣言は通常の変数宣言と基本的に変わりませんが、初期化はできません。構造体変数リストには、この構造体の変数名を指定して構造体変数を定義します。この構造体変数リストに変数名を宣言することで、実際にメモリが割り当てられます。座標情報を保存するための構造体変数は、例えば次のようになるでしょう。
struct Point { int x; int y; } pt ;
このとき、Point 構造体は int 型のメンバ x と y を保有していると考えることができます。この構造体の宣言では、同時に構造体変数 pt を定義しています。構造体変数リストは省略することが可能であり、その場合は構造体の型だけが宣言され、メモリは割り当てられません。構造体変数を任意の場所で宣言したい場合はタグ名を利用して次のように記述します。
struct タグ名 変数名;
struct キーワードは、この変数が指定したタグの構造体変数であることを明示するために必要であり、省略できません。因みに、構造体変数は宣言した構造体の実体であるという考えからインスタンスと呼ぶこともあります。構造体はインスタンスを作成するために、コンパイラに通知するための情報にすぎないということを理解してください。
作成した構造体変数のメンバにアクセスするには、構造体変数名とメンバ名の間にピリオド . を指定します。これをメンバアクセス演算子と呼びます。メンバアクセス演算子は構造体型変数を左オペランドに、左オペランドに指定した変数のメンバを右オペランドに指定する2項演算子です。
構造体変数名 . メンバ名
例えば、先ほどの Point 構造体の変数 pt の x メンバに値 10 を代入したいのであれば pt.x = 10 というように記述します。
#include <stdio.h> int main() { struct Point { int x , y; } pt ; pt.x = 100; pt.y = 50; printf("pt.x = %d : pt.y = %d\n" , pt.x , pt.y); return 0; }
このプログラムは、座標を管理する Point 構造体を宣言し、Point 構造体型の pt 変数を作成しています。メンバアクセス演算子 . を用いてそれぞれのメンバに値を代入し、printf() 関数で各メンバの値を表示しています。
構造体は配列と同様に合成体であることを説明しました。合成体は初期化子を使って初期化することができるためコード1のように静的な値で初期値を与えるのであれば、初期化子を使ったほうが効率的でしょう。つまり変数宣言時に以下のように初期化できます。
struct Point { int x , y; } pt = { 100 , 50 } ;
初期化方法は配列と同じで、先頭から順番に割り当てられているメンバの値を表します。初期化リストの値がメンバ数よりも少ない場合、残りは 0 で初期化されます。
また、Point 構造体はその性質から再利用性が高く、どの関数からも自由にアクセスすることが可能であるべきです。一時的な構造体の場合はコード1のような書き方が推奨されますが、プログラムの多くの場所で使うことができる再利用性の高い構造体は、グローバル領域に記述し、必要なときにインスタンスを作成することができるように設計するべきです。
#include <stdio.h> struct Point { int x; int y; }; int main() { struct Point pt = { 100 , 50 }; printf("pt.x = %d : pt.y = %d\n" , pt.x , pt.y); return 0; }
コード2では、Point 構造体の宣言時に変数を作成していません。構造体のインスタンスは main() 関数内で struct キーワードとタグ名を使って作成していることに注目してください。多くの場合、構造体はこのように利用されます。また、初期値が決まっているのであれば初期化子を用いて初期化することができます。
コード2で作成した Point 構造体のように、構造体そのものの宣言は新しい型情報であり、そのインスタンスは必要なときに開発者が定義して利用します。構造体とは、複数の単純型を合成して新しい型を作り出してしまうようなものであると考えてよいでしょう。インスタンスが生成されるまでメモリは確保されないということと、インスタンスは複数作ることが可能であることを理解してください。
異なる構造体変数はインスタンスが異なるので、メンバの値を共有することはありません。構造体は生成されるインスタンスごとにメモリを割り当てます。構造体がユーザー定義の新しい型情報であるというのはこのためです。一度作成した構造体は(タグ名が定められていれば)何度もインスタンスを作り、その構造を利用することができるのです。
#include <stdio.h> struct Point { int x; int y; }; int main() { struct Point pt1 = { 100 , 50 } , pt2 = { 200 , 100 }; printf("pt1 : x = %d , y = %d\n" , pt1.x , pt1.y); printf("pt2 : x = %d , y = %d\n" , pt2.x , pt2.y); return 0; }
コード3では、2つの構造体変数 pt1 と pt2 を定義しています。これらは、それぞれが独立したメモリ領域を持つため、メンバは共有されません。pt1 の x メンバの値を変更しても pt2 とは物理的に関係がないため pt2 のメンバの値が変化するようなことはないのです。図1はコード3の二つの構造体変数のメモリ構造を簡単に表しています。
このように、構造体のインスタンスはメンバに値を保存するためのメモリ領域を個別に保有しています。pt1.x と pt2.x のメモリアドレスは異なっているため、物理的な関連はありません。
7.1.2 無名構造体
タグ名を省略して、名前のない構造体を作ることも可能です。ただし、タグ名がない場合は構造体の宣言部以外でインスタンスを作成することができないため、この記法が採用されるのは、局所的に利用される構造体の場合だけです。開発者や設計者がプログラムの簡便性のために一時的に必要であると判断した時に使う構造体などで使用します。
#include <stdio.h> int main() { struct { int x; int y; } pt = { 100 , 35 }; printf("pt.x = % d: pt.y = %d\n" , pt.x , pt.y); return 0; }
コード4の main() 関数内で宣言されている構造体はタグ名を省略しているため無名です。この構造体のインスタンスは、このプログラムにおいて pt 変数のみであり、新たにインスタンスを作成することはできません。
7.1.3 構造体と配列
構造体変数は、配列として宣言することも可能です。そうすることによって、構造体のインスタンスをメモリ領域に連続して割り当てることができます。そうすれば、添字やポインタの演算によって総合的に処理にかけることができるようになります。
構造体変数の特定の要素にアクセスするには、変数名の後に添字を指定します。メンバ名の後ろに添字を指定した場合、構造体のメンバを配列として参照することになるので、その違いを理解してください。
#include <stdio.h> struct Item { char *name; int index; }; int main() { struct Item item[5] = { { "George Washington" } , { "John Adams" , 1 } , { "Thomas Jefferson" , 2 } , { "James Madison" , 3 } , { "James Monroe" , 4 } }; int iCount; for(iCount = 0 ; iCount < 5 ; iCount++) { printf( "Name = %s : ID = %d\n" , item[iCount].name , item[iCount].index ); } return 0; }
コード5は、Item 構造体を定義し、この構造体型の配列変数 item[5] を作成しています。Item 構造体はリストとして利用される情報を表しています。name メンバには項目名を、index メンバには項目に与える番号を格納します。何らかの情報管理ソフトウェアを作成する場合、このようにまとめられた形で一括処理できるようにプログラムします。このプログラムでは item 変数の宣言時に全ての要素を初期化し、for 文でリストとして画面に表示させています。
構造体型の配列変数にアクセスする時 item[iCount].name というように添字を指定しています。item.name[iCount] ではないので注意してください。メンバ名の後ろに添字を指定した場合、そのメンバを配列として参照することを表します。
7.1.4 構造体の代入
双方が同じ型の構造体変数であれば、構造体変数を他の構造体変数に代入することができます。これは、純粋にインスタンスを複製するということであり、構造体変数が保有するメンバの値もそっくりコピーされます。
構造体変数1 = 構造体変数2;
こうすると、上の「構造体変数1」は「構造体変数2」とまったく同じ情報を保有することになります。このような代入を行うことで、簡単に構造体変数のコピーを取ることができます。
#include <stdio.h> struct Point { int x; int y; }; int main() { struct Point pt1 = { 200 , 100 } , pt2 = { 0 }; printf("pt2.x = %d : pt2.y = %d\n" , pt2.x , pt2.y); pt2 = pt1; printf("pt2.x = %d : pt2.y = %d\n" , pt2.x , pt2.y); return 0; }
コード6は、Point 型の pt1 変数と pt2 変数を宣言し、それぞれを初期化しています。このとき pt2 変数は 0 で初期化しているため、最初の printf() 文では x メンバも y メンバも 0 という値を表示します。しかし、次に pt2 = pt1 という代入式で pt1 変数を pt2 変数に代入しています。その結果、pt1 変数のメンバがそのまま pt2 変数にコピーされたため、2 回目の printf() 文では pt1 変数と同じ値を表示することでしょう。
この性質は、関数の引数や戻り値で構造体型を受け渡しする時にも応用することができます。引数として構造体型の変数を渡した場合、代入と同様にインスタンスの内容がそのままコピーされます。
#include <stdio.h> struct Point { int x; int y; }; struct Point SizeToPoint(struct Point offsetPoint , int width , int height) { struct Point pt; pt.x = offsetPoint.x + width; pt.y = offsetPoint.y + height; return pt; } int main() { struct Point location = { 100 , 100 }; struct Point target = SizeToPoint(location , 200 , 40); printf( "Rectangle\n\t" "Left = %d : Top = %d\n\t" "Right = %d : Bottom = %d\n" , location.x , location.y , target.x , target.y ); return 0; }
コード7は、ある座標を原点に、指定した幅と高さの座標を求める SizeToPoint() 関数を作成し、この関数と構造体変数を受け渡ししています。SizeToPoint() 関数は、offsetPoint に指定された座標を原点に、幅 width と高さ height から新しい座標を作成してこれを返します。この関数を用いれば、幅と高さを元に長方形の右下隅の座標を得ることができます。 実行結果の Left と Top は長方形の左上角の座標、Right と Bottom は右下角の座標を表しています。プログラムでは右下隅の座標を取得するために、SizeToPoint() 関数を利用しています。
構造体の代入は、全てのメンバを丸ごとコピーするため、メモリ消費とCPUへの負荷が発生する点に注意しなければなりません。値の複製ではなく、単に関数に構造体の値を渡すことが目的であれば、関数を呼び出すたびに構造体を丸ごとコピーする方法よりも、変数へのポインタを渡した方が明らかに効率的です。この方法については、「7.2 構造体へのポインタ」で明らかにします。
プログラムを作成していると、しばしば関数が返す合成体において、合成体の要素全体には興味がなく、特定の要素だけが式中に必要であることが確定している場合があります。例えば、コード7の SizeToPoint() 関数が返す Point 構造体の情報のうち x か y メンバの値が、特定の式で一度だけ必要であるというケースです。このような場合はコード8で示すような記法を使うとスマートになります。
#include <stdio.h> struct Point { int x; int y; }; struct Point SizeToPoint(struct Point offsetPoint , int width , int height) { struct Point pt; pt.x = offsetPoint.x + width; pt.y = offsetPoint.y + height; return pt; } int main() { struct Point location = { 100 , 100 }; printf( "Left = %d : Right = %d\n" , location.x , SizeToPoint(location , 200 , 40).x ); return 0; }
注目するべきは printf() 関数の引数に指定している SizeToPoint(location , 200 , 40).x という式です。まるで SizeToPoint() 関数自体が構造体変数であるかのような書き方をしています。しかし、この書き方は有効です。
関数を式の中で指定した場合、その式において関数は戻り値型の値であると解釈することができます。式を評価する時に関数が実行され、制御が戻ると、その戻り値が式の中で用いられるためです。コード8 の printf() 関数では第 3 引数が評価される時、SizeToPoint() 関数が呼び出されます。SizeToPoint() 関数の戻り値は Point 型であり、この関数は式において Point 型であると考えることができるのです。そのため、SizeToPoint(...).x は、SizeToPoint() 関数が返す Point 型インスタンスの x メンバを参照するということを表しています。