構造体
構造体とは
プログラムがある程度複雑になってくると、一つの概念に対して複数の変数が割り当てられることがあります。例えば学校で学生のデータベースを作る時、その学生の学生番号、名前、年齢 といったデータをひとまとめにして扱う事になります。
そういったプログラムを作る時、関連する変数がバラバラになっていると、取扱が非常に不便です。そこで便利なのが、構造体(こうぞうたい)という概念です。構造体とは、複数の変数をひとまとめにするものです。
たとえば、学生番号を表す整数型の変数id、名前を表す文字列name、年齢を表す整数型変数ageをひとまとめにして構造体にすると、以下のようになります。
構造体テンプレートの定義int id; // 学生番号
char name[256]; // 名前
int age; // 年齢
};
このような、構造体の定義を構造体テンプレートと言います。structが、構造体を表すキーワードであり、そのあとのstudentが構造体の名前になります。 構造体の名前は、任意につけることができます。{}の中に、ひとまとめにする変数を定義します。最後は;(セミコロン)で終了します。
この構造体を実際に使用するには、
構造体変数の定義とすると、dataという名前の構造体変数を定義できます。
サンプルプログラム
では、実際に来れに代入したり、出力したりするサンプルを見てみましょう。
listex5-1:main.c#include <stdio.h> #include <string.h> // 学生のデータを入れる構造体 struct student{ int id; // 学生番号 char name[256]; // 名前 int age; // 年齢 }; void main(){ struct student data; data.id = 1; // 番号を設定 strcpy(data.name,"山田太郎"); // 名前を設定 data.age = 18; // 年齢を設定 // データの内訳を表示 printf("学生番号:%d 名前:%s 年齢:%d¥n",data.id,data.name,data.age); }
構造体の成分の変数のことを、メンバと言います。このメンバにアクセスするには、通常以下のように行います。
構造体変数の成分へのアクセスこのサンプルでは、構造体変数はdataなので、そのメンバidへのアクセスは、間に"."(ピリオド)をつけて、"data.id"とします。このサンプルでは、13~15行目で、 それぞれの成分に値を代入しています。先頭に"data."がついているだけで、それぞれの成分へのアクセスは、普通の変数とは変わりません。
C言語には、このほかに、構造体によくにた共用体(きょうようたい)という概念も存在します。興味のある方は、以下のサイトを参考にしてみてください。
→ 共用体について
構造体配列
サンプルプログラム
次は、構造体を配列にして使用する例を紹介します。以下のプログラムを実行してみてください。
listex5-2:main.c#include <stdio.h> #include <string.h> // 学生のデータを入れる構造体 struct student{ int id; // 学生番号 char name[256]; // 名前 int age; // 年齢 }; // 構造体の名前をtypedefで定義 typedef struct student student_data; void main(){ int i; student_data data[] = { { 1,"山田太郎",18 }, { 2,"佐藤良子",19 }, { 3,"太田隆",18 }, { 4,"中田優子",18 } }; // データの内訳を表示 for(i = 0; i < 4; i++){ printf("学生番号:%d 名前:%s 年齢:%d¥n",data[i].id,data[i].name,data[i].age); } }
学生番号:2 名前:佐藤良子 年齢:19
学生番号:3 名前:太田隆 年齢:18
学生番号:4 名前:中田優子 年齢:18
typedef
配列変数について説明する前に、以下の処理について説明しましょう。
構造体名の変更先頭に出ているtypedefは、既存の型に新しい名前(別名)を付けるためのキーワードで、このプログラムの場合、studentという構造体をstudent_dataというなまえに変更するという事を意味します。
再定義した構造体でのデータの定義のように、先頭に"struct"キーワードをつけることなく構造体変数を定義することが可能です。
次に、構造体変数への値の代入ですが、初期値の設定の場合、16行目から21行目のように、通常変数の場合のように、{}を使って値を一度に複数定義することができます。外側の{}の中に、定義する値の数だけ、{}でメンバを定義して、間を,(コンマ)で区切ります。メンバの値の定義は、メンバの並び順に正しく代入する必要があります。
構造体のポインタ
サンプルプログラム
最後に、構造体とポインタの使い方について説明していきましょう。以下のプログラムは、listex5-2と同じ処理をポインタを使った処理に書き換えたものです。 少し長いですが、入力して実行してみてください。
listex5-3:main.c#include <stdio.h> #include <string.h> // 学生のデータを入れる構造体 typedef struct{ int id; // 学生番号 char name[256]; // 名前 int age; // 年齢 }student_data; // 構造体のデータを表示する関数 void setData(student_data*,int,char*,int); void showData(student_data*); void main(){ student_data data[4]; int i; int id[] = { 1,2,3,4 }; char name[][256] = { "山田太郎","佐藤良子","太田隆","中田優子" }; int age[] = { 18,19,18,18 }; // データの設定 for(i = 0; i < 4; i++){ setData(&data[i],id[i],name[i],age[i]); } // データの内訳を表示 for(i = 0; i < 4; i++){ showData(&data[i]); } return; } // データのセット void setData(student_data* data,int id,char* name,int age){ data->id = id; // idのコピー strcpy(data->name,name); // 名前のコピー data->age = age; // 年齢のコピー } // データの表示 void showData(student_data* data){ printf("学生番号:%d 名前:%s 年齢:%d¥n",data->id,data->name,data->age); }
実行結果は、listex5-1と同じなので省略します。構造体ポインタに関する説明をする前に、5~9行目を見てください。
構造体テンプレートの定義と、名前の変更の一括処理int id; // 学生番号
char name[256]; // 名前
int age; // 年齢
}student_data;
アロー演算子
この処理を行うと、構造体テンプレートの定義と、名前の変更が同時にできます。したがって、16行目のように、"struct"キーワード抜きで構造体変数を定義できます。
次に、本題であるポインタの構造体について説明しましょう。通常の構造体では、メンバにアクセスするのに"."を用いますが、ポインタの場合は、"->"(アロー演算子)を用います。(表5-1)
通常の構造体 | 構造体のポインタ | |
---|---|---|
定義 | student_data data | student_data* pData |
メンバ | data.id data.name data.age | data->id data->name data->age |
showData()関数およびshowData()関数では、ポインタの形式でデータが渡ってきます。そのため、33~35行目、もしくは39行目では、アロー演算子が用いられています。ここで、値の表示およびデータの代入が行われています。(図5-1)
![]() |
通常、構造体を関数の引数として渡す場合は、このサンプルのようにアドレスを渡すのが普通です。では、いったいなぜそのようなことをするのでしょう?次でそのことを詳しく説明しましょう。
ポインタ渡しとデータ渡し
サンプルプログラム
ポインタ渡しとデータ渡しの違いを理解するために、まずは以下のプログラムを入力・実行してみてください。
listex5-4:main.c#include <stdio.h> // データを入れる構造体 typedef struct{ int a; double d; }num_data; // 二種類の値設定関数 void dealData1(num_data data); // 値渡し void dealData2(num_data* pData); // ポインタ渡し void main(){ num_data n1 = { 1, 1.2f },n2 = { 1, 1.2f }; printf("n1のアドレス:0x%x n2のアドレス:0x%x\n",&n1,&n2); dealData1(n1); dealData2(&n2); printf("n1.a = %d n2.d = %f¥n",n1.a,n1.d); printf("n2.a = %d n2.d = %f¥n",n2.a,n2.d); } void dealData1(num_data data) { printf("a=%d f=%f\n",data.a,data.d); printf("dealData1にわたってきたデータのアドレス:0x%x¥n",&data); // 値の変更 data.a = 2; data.d = 2.4; } void dealData2(num_data* pData) { printf("a=%d f=%f\n",pData->a,pData->d); printf("dealData2にわたってきたデータのアドレス:0x%x¥n",pData); // 値の変更 pData->a = 2; pData->d = 2.4; }
a=1 f=1.200000
dealData1にあたってきたデータのアドレス:0x2cf630
a=1 f=1.200000
dealData2にあたってきたデータのアドレス:0x0x2cf710
n1.a = 1 n2.d = 1.200000
n1.a = 2 n2.d = 2.400000
実行結果より、値渡しの場合は、dealData1()関数に渡った引数のアドレスはもとの数とは異なります。ポインタ渡しのdealData2()の場合は、同じアドレスとなります。 そのため、showData1()では、値の変更を行っても、main()には反映されませんが、showData2()の場合は、アドレスが同じであることから、値の変更が反映されます。
これが、データ渡しとポインタ渡しの違いです。
データ渡しの問題点
ポインタを渡す理由は二つあります。一つは、通常、構造体のデータのサイズは大きくなる傾向があり、引数としてそのままの値を渡すと、スタック領域を圧迫してしまったり、データのコピーという無駄な処理が起こり、二重の意味でリソースを無駄にしてしまうからです。
そして、もうひとつの理由は、ポインタ渡しであれば、関数の中で値の設定などが出来るからです。値渡しでは、前述のようにコピーが発生する上に、引数として渡ってきた構造体のデータを変更しても、呼び出しもとの値に反映されません。(図5-2)
![]() |
以上のような理由から、構造体を関数の引数として渡す場合は、ポインタ渡しを用いるのが普通なのです。