Unix C プログラミング入門資料(日置尋久・高崎金久)   10.構造体 目次 1.構造体とは 2.構造体の利用法 3.構造体の応用 -------------------------------------------------------------------------- 1.構造体とは 構造体とは,いろいろなデータをまとめて扱う仕組みを提供するもので,ユーザが 独自に定義する新しいデータ型として使用できます.構造体を用いることによって, データの意味を明確にして,プログラムをわかりやすくすることができます.構造 体は次のように定義します. struct 構造体名 { 型1 フィールド名1; 型2 フィールド名2; : : }; たとえば,次のように定義できます. struct member_data { int id; char name[NAMELEN]; char addr[ADDRLEN]; int age; }; member_data は(何かの会の)会員データを表す構造体で,4つのフィールドをも ちます.この構造体を新しい型として使うには,普通の変数と同じように,たとえ ば, int main(int argc, char *argv[]){ struct member_data mdata; のように宣言します.このとき必ずキーワード『struct』を構造体名の前につける ようにします.次のように構造体を定義するのと同時に変数を宣言することもでき ます. struct foo { int x,y; char ch; } bar,baz; int main(int argc, char *argv[]) { bar.x = baz.x = 0; bar.y = baz.y = 1; bar.ch = 'A'; baz.ch = 'B'; しかしこの書き方はあまりお薦めしません.「構造体定義」と「変数宣言」は分け た方がよいでしょう.また構造体名を省略した「無名の構造体」を定義して使うこ ともできます. void useless_func(int i, char *s) { struct { int i; char *s; } anon; anon.i = i; anon.s = s; return; } このような書き方の場合,定義した構造体を型として他の場所で利用することはで きません(注). (注) typedef とともに使えば型として有効になります. 2.構造体の利用法 2.1 構造体のフィールドへの代入,フィールドの参照 構造体のフィールドへの値の代入,フィールドへの参照などは以下のように行いま す. struct vector { double x,y; }; #define SQU(x) ((x)*(x)) int main(int argc, char *argv[]) { struct vector v,w; double l; v.x = atof(argv[1]); v.y = atof(argv[2]); v.z = atof(argv[3]); l = sqrt(SQU(v.x) + SQU(v.y) + SQU(v.z)); fprintf(stderr, "|(%f,%f,%f)|=%f\n", v.x,v.y,v.z,l); w = v; fprintf(stderr, "w=(%f,%f,%f)\n", w.x,w.y,w.z); return 0; } つまり『変数名.フィールド』で,フィールドを構成する変数にアクセスできるわ けです.なお最後に書いたとおり,構造体については = で直接代入を行うことが できます(この場合,w は v と同じベクトルになります).従って,以下のよう なこともできます. #define SIZE 0; struct { int a[SIZE}; } iarray; int main(void){ struct iarray a,b; int i; for(i = 0; i < SIZE; i++) a.a[i] = i; b = a; /* 構造体のコピー */ for(i = 0; i < SIZE; i++) fprintf(stderr, "%d", b.a[i]); fprintf(stderr,"\n"); return 0; } このようにすれば,配列全体をコピーすることができてしまいます.ただし構造体 のフィールドがポインタであった場合は,そのポインタの値(アドレス)がコピー されるだけで,ポインタの指す先のデータがコピーされないことに注意して下さい. 2.2 構造体の配列,構造体へのポインタ 構造体は新しい型を定義するわけですから,基本型と同様に,構造体の配列,構造 体へのポインタを使うことができます.構造体の配列は,たとえば以下のように使 います. int main(int argc, char *argv[]) { struct vector a[3],b[3],c[3]; int i; : for(i = 0; i < 3; i++) { a[i].x = b[i].x + c[i].x; a[i].y = b[i].y + c[i].y; a[i].z = b[i].z + c[i].z; } : } 構造体を指すポインタ変数には,基本型のポインタとは異なる点があります.まず, ポインタが指し示す構造体のフィールドを参照するにはどう記述すればよいのか考 えてみてください. 記述 意味 ------ ---------------------------------------------- *v.x 構造体 v のフィールド x の指し示す値.つまり *(v.x) と同じ.x はポインタ. ------ ---------------------------------------------- (*p).y ポインタ p の指す構造体のフィールド y の値. ------ ---------------------------------------------- ポインタの指す先のフィールドは *v.x のように書けばよさそうですが,演算子 の優先順位から,それでは別の意味になってしまいます.それを防ぐには括弧をつ けて明示的に優先順位をつける必要があるのですが,構造体へのポインタは頻繁に 使われるため,特別な演算子 -> が用意されています.(*p).x と p->x は同じ意 味になります. int normalize_vectors(struct vectors *a,int N); int main(int argc, int *argv[]) { struct vector a[3]; : if(normalize_vectors(a,3) < 0) return 1; : } /* N 個のベクトルを単位ベクトルに変換する */ int normalize_vectors(struct vector *a, int N) { double l; int i; if(a == NULL) return -1; for(i = 0; i < N; i++,a++) { l = sqrt(SQU(a->x)+SQU(a->y)+SQU(a->z)); a->x /= l; a->y /= l; a->z /= l; } return 0; } ちなみに構造体を使わなくてもプログラムを書くことはできます.たとえば,上の 例のプログラムを構造体を使わないで書いた場合,次のようになります(関数の名 前をわざと変えてあります). int unit_v(double *x, double *y, double *z, int N) { double l; int i; if(x == NULL || y == NULL || z == NULL) return -1; for(i = 0; i < N; i++,x++,y++,z++) { l = sqrt(SQU(*x)+SDQU(*y)+SQU(*z)); *x /= l; *y /= l; *z /= l; } return 0; } このようなプログラムでも,記述を追えば,どうやらベクトルの演算らしいとはわ かります.しかし構造体を使った方がプログラムの意味が明瞭で,記述も簡単にな ります. 2.3 構造体をフィールドにもつ構造体 構造体の定義を思い出すと,フィールドはある有効な型の変数ですから,構造体を 別の構造体のフィールドに指定することができます.つまり構造体のフィールドを 分類して階層化することができるわけです. /* 点,ベクトル */ typedef struct { double x,y,z; } point,vector; /* 直線 */ typedef struct { point g; vector ;: } line; int main(void) { line p,*q; p.g.x = p.g.y = p.g.z = 0.0; p.l.x = 1.0; p.l.y = p.l.z = 0.0; q = &p; fprintf(stderr, "k(%f,%f,%f)+(%f,%f,%f)\n", q->l.x, q->l.y, q->l.z, q->g.x, q->g.y, q->g.z); return 0; } もちろん構造体をもつ構造体にしないで,同じ意味をもつ構造体を定義することは できますが,その場合,すべてのフィールドを別の名前にしなければなりませんし, それぞれのフィールドの意味が不明瞭になります. 余談:typedefについて 上の例では,typedef宣言により,構造体で表される型に名前を付けています. typedef宣言の構文は変数宣言の構文と同じです. typedef データ型 名前 複数の名前を同時に付けることもできます. typedef int integer,*integer_ptr; typedef int integer_array[10]; int main(void) { /* * i は整数 * p,q はともに整数へのポインタ * a は長さ 10 の整数の配列 * x は整数へのポインタ,y は整数 */ integer i; integer_ptr p,q; integer_array a; int *x,y; p = &i; i = 1; fprintf(stderr,"%d\n",*p); for(i = 0; i < 10; i++) a[i] = i; for(i = 0, q = a; i < 10; q++,i++) fpritnf(stderr,"%d ",*q); fprintf(stderr,"\n"); return 0; } 3.構造体の応用 3.1 構造体による基本的なデータ構造の実現 構造体を用いることでリスト(list)や木(tree)のようなデータ構造を定義する ことができます. 線形リスト ------ ------ ------- | |-|----->| |-|------> ... ----->| | | ------ ------ ------- /* * int を要素とするリストセルの定義 * next が次のセルを指す * 最後のセルの next は NULL */ struct cell { int i; struct cell *next; }; /* * int を要素とする二分木のノードの定義 * left, right が左右の子ノードを指す * left, right がNULLならば子ノードはない */ struct node { int i; struct node *left; struct node *right; }; 3.2 サンプルコード 整数の引数 n を受け取り,順に 0 から n-1 までを要素とする n 個のセルの並ん だリストをつくります.セルの領域を確保できなかった場合には,その時点で実行 を終了します.このコードでは確保した領域を解放していません.またここでは head をダミーとして使っています.head はデータをもたないセルですが,これを 使うことでコードが簡潔になります. int main(int argc, char *argv[]) { struct cell *p,*c,head; int i,n; if(argc == 1) return 1; n = atoi(argv[1]); head.next = NULL; p = &head; for(i = 0; i < n; i++) { if(NULL==(c=malloc(sizeof(struct cell)))) return 2; c->i = i; p->next = c; c->next = NULL; p = c; } for(c=head.next; c != NULL; c=c->next) fprintf(stderr,"%d ",c->); fprintf(stderr,"\n"); return 0; }