数十人程度の生徒が受けた試験の、数学の得点と英語の得点を集計する場合
を考えよう。最大100人程度の配列を作る。今後、教科が増えることを考えると
math, eng などといった違う名前の配列にそれぞれの得点を入れるのは
効率が良くない。このようなときには、色々な種類の値をひとまとめにした
複合型を用いると良い。int, char, long, float[]
などC言語
で扱える全ての方を自由な組み合わせで好きなだけ集めた型を作ることができる。
これを構造体と呼ぶ。
ある試験を受けた学生一人一人がそれぞれ持っている「値」を考えると (必要になりそうなものを多めに盛り込むと)
となる。この試験に関しては、1人についてこの4つの情報を持っていれば 集計もできそうだし、成績表の印刷などする必要が出ても対応できそうである。
これらの情報をC言語で使えるデータとしてどの型に保存するかを 考えると以下のようになる。
値の種類 | C言語での型 |
---|---|
氏名 | 文字列(char [] ) |
出席番号 | 整数(int ) |
数学得点 | 整数(int ) |
英語得点 | 整数(int ) |
以上をまとめたものを構造体として定義する。
struct tokuten {
char name[20]; /* 最大20バイトで足りるかな */
int shusseki; /* 出席番号は int */
int mathpt; /* 数学の得点も int */
int engpt; /* 英語の得点も int */
}; /* セミコロンが必要!! */
キーワード struct
の後に作りたい構造体の名前を
つけて定義する。変数名と同様、アルファベットで始まる自由な単語を
つけて良い。構造体の中にいくつか列挙した細かい変数のことを
構造体のメンバという。
上記のような宣言により、新しい構造体 struct tokuten
を使うことができる。自分で定義した構造体は、一つの変数の型として
使うことができる。つまり、その構造体を型に持つ変数を宣言できる。
例:
struct tokuten taro, hanako;
このように書けば、struct tokuten型の変数
taro と hanako
が用意される。taro, hanako
いずれの変数も、内部に4つのメンバを持つ変数となる。
taro
char
name[20]
int
shusseki
int
mathpt
int
engpt
hanako
char
name[20]
int
shusseki
int
mathpt
int
engpt
taro
に値を代入してみよう。構造体 struct tokuten
は、
その内部に4つのメンバ、name, shusseki, mathpt, engpt
を持つので、構造体に値を代入する場合は、内部のメンバ全てに
代入することになる。構造体のメンバにアクセスするには
構造体の変数にピリオドをつけてメンバ名を書く。つまり以下のようにする。
/* taro.nameに "公益太郎" を代入。文字列はstrlcpyしないとダメ */
strlcpy(taro.name, "公益太郎", 20);
taro.shusseki = 1; /* 出席番号は1 */
taro.mathpt = 80; /* 数学得点は 80 */
taro.engpt = 70; /* 英語得点は 80 */
【復習】 既に確保してあるchar型配列に文字列を代入するときは
strlcpy
を使う。イコール(=)で代入はできない。
上記の代入を変数の宣言と同時に行なうこともできる(本格的な プログラムではほとんど使わないが、プログラミングの練習のときは この書き方を知らないと不便)
struct tokuten hanako = {"飯森花子", 2, 90, 60};
たったいま宣言・確保した構造体の変数 taro
を別の関数に
渡して、そこでなんらかの計算を行なってもらう場合、別の関数に構造体の値を
渡す方法には2種類ある。一つは値渡しで、もう一つはアドレス渡し
である。結論から言うと後者のアドレス渡しの方が実践的だが、値渡しの方が
簡単なので両方覚えておこう。
構造体に限らず、変数を別の関数に引数として渡すときに
変数の中味をコピーして渡すことを値渡し(call by value)という。
たとえば、以下の関数sub
は値渡しである。
#include <stdio.h>
int add3(int x)
{
x = x+3;
printf("結果は%d\n", x);
}
int main()
{
int a=4;
add3(a); /* 値(のコピー)を渡している */
printf("a=%d\n", a);
}
このような関数呼出しでは、add3
関数には
a
の中味である4がコピーされて、それがadd3
関数の仮引数の x
に代入される。したがって main
関数中のa
と、add3
関数中の変数x
は全く別の場所に確保されている。ただし、それらの中味はどちらも(最初は)4
になっている。add3
関数の方で
x = x+3;
として値を変更しているが、これはmain
関数中のa
には全く影響が出ない。
これとは別に、変数のアドレスを渡すのがアドレス渡し (call by address)である。別の関数で、値を変えて欲しいときなどには アドレス渡しが必要になる。以下の例を見てみよう。
#include <stdio.h>
void nibai(int *x)
/* アドレス渡しの仮引数はポインタ変数で宣言する */
{
*x *= 2; /* 2倍するだけ */
}
int main()
{
int y=3;
nibai(&y) /* yのアドレスを渡す */
printf("y=%d\n", y); /* → 6になる */
}
構造体を別関数に渡すとき、その値のコピーをとって渡すには 変数名をそのまま書いて渡せば良い。
#include <stdio.h>
struct tokuten {
char name[20];
int shusseki;
int mathpt;
int engpt;
};
void dispall(struct tokuten p)
{
printf("%sさん(出席番号%d)は数学%d点、英語%d点でした\n",
p.name, p.shusseki, p.mathpt, p.engpt);
}
int main()
{
struct tokuten taro = {"公益太郎", 1, 80, 70};
struct tokuten hanako = {"飯森花子", 2, 90, 60};
dispall(taro);
dispall(hanako);
}
受け取る関数 dispall
のほうでも、仮引数p
で受け取ったなら
p.メンバ名
によってそれぞれのメンバの値を取り出すことができる。
構造体のアドレスを別関数に渡すときには、受け取り側の関数では 構造体のポインタとして受け取る。
#include <stdio.h>
struct tokuten {
char name[20];
int shusseki;
int mathpt;
int engpt;
};
void dispall(struct tokuten *x)
{
printf("%sさん(出席番号%d)は数学%d点、英語%d点でした\n",
x->name, x->shusseki, x->mathpt, x->engpt);
}
int main()
{
struct tokuten taro = {"公益太郎", 1, 80, 70};
struct tokuten hanako = {"飯森花子", 2, 90, 60};
dispall(&taro);
dispall(&hanako);
}
受け取り側の関数 dispall
では仮引数を構造体への
ポインタ変数として宣言する。重要なのは、構造体のポインタとして受けた変数
から各メンバを取り出すにはピリオドではなく ->
を使う。
つまり、
struct foo *p (構造体へのポインタ)の場合は p->name p->shusseki p->mathpt p->engpt
のように記述する。
ポインタ変数 x
はアドレスを示す。アドレスを持っている
変数から実際の値を取り出すには * 印を頭につけるので、*x
で中味が取り出せることになる。つまり、struct tokuten *p
という変数なら、*p
が構造体を示すことになるので、
その中のメンバを取り出すには、(*p).name
と書くのが
もっとも理にかなっている。しかし構造体のポインタを受け取ることは
実践では頻繁に用いられるので (*p).name
と書く代わりに
p->name
と書いて良いことになっている。確かにタイピング
は速くできるけど、理解するのがややこしいね。
構造体を宣言するときに、一人一人のデータを別々の変数として宣言する 事はほとんどしない(だって面倒だから)。たとえば、
struct tokuten taro, hanako;
というに宣言してプログラムを書くことは現実にはほとんどない(プログラミン グの練習のときには良く使う)。通常は、構造体を配列として宣言して、何人も のデータが一度に扱えるようにする。
struct tokuten pointdata[100];
のように宣言すると、100人分の「氏名、出席番号、数学得点、英語得点」を 保存できる構造体の配列が確保される。
pointdata[0].name
は、0番目の構造体のnameメンバ(つまり氏名)へのアクセスを意味し、
pointdata[50].mathpt
は、50番目の構造体のmathptメンバ(つまり数学得点)へのアクセスを意味し、
pointdata[i].engpt
は、i番目の構造体のengptメンバ(つまり英語得点)へのアクセスを意味する。
このようにして宣言した配列を別関数に渡すには配列としての先頭アドレス
を渡し、受け取る側では配列変数として受け取れば良い。
例:
/* 受け取る側 */
void shuukei(struct tokuten v[])
{
/* v[i].name, v[i].shusseki, v[i].mathpt, v[i].engpt
が参照できる */
}
/* 渡す側 */
int main()
{
struct tokuten pointdata[100];
:
:
/* 用意した配列に順次データを入力する処理 */
:
:
shuukei(pointdata);
/* pointdata だけ書くと配列の先頭アドレス */
}
ただし、Cでは配列に実際に入っている要素数を調べられないので 配列要素が実際に何個入っているかを別の関数に教える必要がある。
/* 受け取る側 */
void shuukei(struct tokuten v[], int numdata)
{
/* v[i].name, v[i].shusseki, v[i].mathpt, v[i].engpt
が参照できる */
}
/* 渡す側 */
int main()
{
struct tokuten pointdata[100];
:
:
/* 用意した配列に順次データを入力する処理 */
/* データの個数が n に残っているとすると… */
:
:
shuukei(pointdata, n);
}