構造体

■ 4-1 構造体の必要性

配列はたくさんある同じようなデータを一まとめにするときに用いますが、データ型が同じデータしかまとめることが出来ません。そのため、例えば1000人分の学生の氏名と5教科の点数を管理するようなプログラムを考えると次のようになります。

#include <stdio.h>

main(){
  char name[1000][21];
  int kokugo[1000];
  int suugaku[1000];
  int rika[1000];
  int syakai[1000];
  int eigo[1000];
  
  .....
}

このプログラムでは、氏名は配列nameでまとめて、国語の点数は配列kokugoでまとめて、というように管理します。つまり、科目ごとに点数を管理するわけです。しかし、この方法では、30番目の学生のデータを900番目の学生のデータとしてコピーするには、

strcpy(name[900], name[30]);   /* 氏名をコピー */
kokugo[900] = kokugo[300];     /* 国語の点数をコピー */
suugaku[900] = suugaku[300];   /* 数学の点数をコピー */
rika[900] = rika[300];         /* 理科の点数をコピー */
syakai[900] = syakai[300];     /* 社会の点数をコピー */
eigo[900] = eigo[300];         /* 英語の点数をコピー */

というように、6行もの操作が必要になります。これでは、管理する学生に関するデータが増えれば増えるほどプログラムが煩雑になります。氏名とそれぞれの科目の点数を分けて管理しているために、このようなことが起こるわけです(下図:この方法では、各教科の点数の一番目が山田太郎の点数、二番目が田中次郎の点数、...となっています)。

では、5科目の点数を一つの2次元配列tensuuにまとめて格納してみればどうでしょう(下図)。

かなりすっきりしましたが、入れ替えを行う場合には以下のような面倒な処理が必要になります。

strcpy(name[900], name[30]);   /* 氏名をコピー */
for(i=0, i<5; i++){
  tensuu[900][i] = tensuu[300][i];     /* 各科目の点数をコピー */
}

仮に、下図のように学生ごとに氏名と点数をまとめることができれば、「30人目の学生のデータを900番目の学生のデータとしてコピー」という操作ができるわけです。つまり、gakusei[900] = gakusei[30]という非常にシンプルな記述が可能になります。

しかし、氏名はchar型の配列、各教科の点数はint型ですからデータ型が違います。そのため、配列を用いて一つにまとめることが出来ません。そこでC言語では、「型が異なるデータを一つにまとめる」機能として構造体が用意されています。

構造体はintやcharなどの基本データ型を好きなように組み合わせて、intやcharなどのデータ型と同じようなデータ型を新しく作る機能です。そのため、構造体の利用には、(1) 構造体の定義、(2)定義された構造体の型の変数(や配列)を宣言、の二つのステップが必要になります。


■ 4-2 構造体の定義とメンバ

構造体の定義とは、その構造体がどんな型の変数や配列を含むかという構造体の形を定義し、その構造体に対して名前を付けることです。構造体の定義は以下のように行います。

struct 構造体名 {
  データ型  メンバ名1;     ← この一行が一つのメンバ
  データ型  メンバ名2;
  ...
};
ここで、データ型とは、intやcharなどの既存のデータ型です。実際には、定義済みの構造体の名前も指定することが可能ですが、これについては後述します。メンバとは構造体を構成する変数や配列のことです。構造体の宣言は、その構造体の中のメンバのデータ型と名前を列挙することで行います。
使用例
struct gakusei {
  char name[21];
  int kokugo;
  int suugaku;
  int rika;
  int syakai;
  int eigo;
};

この例では、nameという名前の文字列(要素数21)、int型のkokugo, suugaku, ..., eigoの変数をメンバとして持つ構造体gakuseiを定義しています。なお、このgakuseiのような構造体の名前を構造体タグと呼びます。


■ 4-3 構造体型変数の宣言と初期化

構造体の宣言は、「構造体がどのようなデータ型と名前のメンバをもっているか」を決めただけです。つまり、新しいデータ型の形を決めただけです。先の例では、「struct gakusei型」という新しいデータ型を定義したということになります。しかし、構造体を実際に使用するには定義された構造体型の変数を宣言する必要があります。

構造体型の変数を宣言するには構造体タグを利用します。以下の例では、図のようなメンバを持つ構造体gakusei型の変数xを宣言しています(変数xの内容は未定です)。このように、構造体タグは既存のデータ型と全く同じように使用することが出来ます

使用例
struct gakusei x;   ←  struct gakuseiはintやchar等と同じように使える。

ここで、「データ型の名前がstruct gakusei型というのは長くて面倒だから、intやcharのように1単語で表せるようにしたい」という場合があります。実際に大規模なプログラムを作成してみると分るのですが、struct gakusei型というデータ型を多用しているソースプログラムは読みにくいものです。そのため、typedef命令を用いて新たなデータ型名を定義することが一般的に行われています。

typedef 既存のデータ型 新たなデータ型名;
ここで、既存データ型とは、intやcharなどのデータ型です。
使用例
typedef int XYZ;

このように書くと、これ以降、XYZ x;と書けば、int x;と書いたことと同義になります。

これを用いて、構造体gakusei型をGAKUSEI型とでも定義しておきましょう。

使用例
struct gakusei {  <--+
  char name[21];     |
  int kokugo;        |
  int suugaku;       |
  int rika;          |- ここで構造体を定義 
  int syakai;        |
  int eigo;          |
};                <--+
typedef struct gakusei GAKUSEI; <--- ここでGAKUSEI型を構造体gakusei型
                                     と同じデータ型として定義

GAKUSEI x;       <--- 以降は、このように利用できる。
                      これは、struct gakusei x;と同義。

さらに、以下のように構造体の定義とtypedefによる型定義を一行で行うことも出来ます。実際にはこちらのほうが一般的に用いられています。

使用例
typedef struct gakusei {  <--+
  char name[21];             |
  int kokugo;                |
  int suugaku;               |
  int rika;                  |- ここで構造体を定義し、かつ、 GAKUSEI型を
  int syakai;                |  構造体gakusei型と同じデータ型として定義
  int eigo;                  |
} GAKUSEI;                <--+

GAKUSEI x;                <--- 以降は、このように利用できる。これは、
                               struct gakusei x;と同義。

さて、ここまでで構造体型の変数を宣言したことになりますが、構造体型の変数も通常のint型やchar型の変数と同様に宣言時に初期化を行うことが可能です。例えば、GAKUSEI型(つまり、構造体gakusei型)の変数xの宣言時に山田太郎君のデータで初期化する方法は以下のようになります。

使用例
GAKUSEI x = {"山田太郎", 100, 94, 40, 87, 87};

これは、変数や配列、文字列の初期化の構文を{}でくくった形です。この文で変数xは以下のように初期化されます。

■ 4-4 メンバの参照

4-3までで構造体を定義し、その構造体型の変数を宣言して初期化しました。ここでは、構造体のメンバの値を参照する方法について説明します。

構造体のメンバを参照するには、メンバ演算子「.(ドット演算子)」を用います。例えば、GAKUSEI型の変数xのメンバkokugoの値を参照するには、x.kokugoと書きます。

使用例
x.kokugo = 80;   <--- これにより、GAKUSEI型(構造体gakusei型)
                      の変数xのメンバkokugoが80に変更される。


演習問題4-1

以下の仕様を満たすプログラムを作成せよ。

  1. 氏名(最大20バイト)、5教科の各点数、平均点をメンバに持つ構造体gakuseiを宣言し、それをGAKUSEI型として定義している。
  2. キーボードから氏名、5教科の点数の入力を受け付ける。
  3. 氏名、5教科の平均を表示する。

演習問題4-1の解答


レポート課題4-1

以下の仕様を満たすプログラムを作成せよ。

  1. 氏名(最大20バイト)、5教科の各点数、平均点、最高点、最低点をメンバに持つ構造体gakuseiを宣言し、それをGAKUSEI型として定義している。
  2. キーボードから氏名、5教科の点数の入力を受け付ける。
  3. 氏名、5教科の点数、5教科の平均、5教科のうちの最高点と最低点を表示する。


■ 4-5 構造体の配列

これまで説明してきたように構造体は既存のデータ型と全く同じように利用できます。ということは、構造体型の配列も利用できるということです。構造体の配列の宣言は通常の配列と全く同様です。

使用例
struct gakusei seito[10];   <--- 10人分の学生のデータを格納
                                 できる構造体の配列。
GAKUSEI seito[10];          <--- こう書いても良い。

構造体の配列を宣言し、同時に初期化するには以下のように記述します。

使用例
GAKUSEI seito[4] = {
  {"山田太郎", 100, 94, 40, 87, 87}, <--- この行で配列の要素一つを初期化
  {"田中次郎",  30, 87, 30, 65, 45},
  {"鈴木三郎",  45, 45, 60, 87, 29},
  {"佐藤花子",  87, 69, 39, 96, 45}
};

このときのGAKUSEI型の配列seitoは下図のようになります。ここで、配列seitoの第2番目の要素のメンバeigoにアクセスするには、seito[2].eigoと記述します。


演習問題4-2

以下の仕様を満たすプログラムを作成せよ。

  1. 氏名(最大20バイト)、5教科の各点数、平均点をメンバに持つ構造体gakuseiを宣言し、それをGAKUSEI型として定義している。
  2. 4人分の氏名、5教科の点数を構造体の配列の初期値として格納している(氏名と5教科の点数は適当に決めてよい)。
  3. 4人分の氏名、5教科の平均を表示する。

演習問題4-2の解答


レポート課題4-2

以下の仕様を満たすプログラムを作成せよ。

  1. 氏名(最大20バイト)、5教科の各点数、平均点、最高点、最低点をメンバに持つ構造体gakuseiを宣言し、それをGAKUSEI型として定義している。
  2. キーボードから最大20名分の氏名、5教科の点数の入力を受け付ける。但し、氏名に"Q"と入力した場合は学生データの入力が終了したものとする。
  3. 入力された学生の情報を5教科の平均の高い順に並び替える。
  4. 並び替えた結果を表示する。


■ 4-6 入れ子の構造体

構造体はそのメンバに別の構造体を含むことが可能です。例えば、これまでのプログラムでは学生のデータを管理するために以下のような構造体を考えてきました。

typedef struct gakusei { 
  char name[21];
  int kokugo; 
  int suugaku;
  int rika;
  int syakai;
  int eigo;
  int max;
  int min;
  float heikin;
} GAKUSEI;

GAKUSEI seito;

しかし、この構造体には、点数に関するメンバが9つもあり、見にくくなっています。また今後、科目数やそれ以外のメンバが増えていくと、さらに見にくくなってしまいます。そこで、科目の点数に関するメンバだけを別の構造体として管理することを考えます。

typedef struct kamoku{  <--- 科目に関するデータのための構造体
  int kokugo; 
  int suugaku;
  int rika;
  int syakai;
  int eigo;
} KAMOKU;
  
typedef struct gakusei { 
  char name[21];
  KAMOKU tensuu;       <--- 構造体kamoku(KAMOKU型)をの変数を
  int max;                  メンバに持つ
  int min;
  float heikin;
} GAKUSEI;

GAKUSEI seito;

このようにして宣言されたGAKUSEI型の変数seitoのなかには、下図のように、KAMOKU型の変数(構造体)が入れ子になっています。

入れ子になった構造体のメンバにアクセスするには、ドット演算子を用います。

使用例
seito.tensuu.kokugo = 100;

この例では、上図の変数seitoのメンバtensuuのkokugoのメンバを100に変更しています。


演習問題4-3

演習問題4-2を構造体の入れ子を用いて作成せよ。

演習問題4-3の解答


■ 4-7 構造体へのポインタ

構造体型の変数は通常の変数と同じようにポインタで操作することが出来ます。

使用例
typedef struct gakusei { 
  char name[21];
  int kokugo; 
  int suugaku;
  int rika;
  int syakai;
  int eigo;
  int max;
  int min;
  float heikin;
} GAKUSEI;

GAKUSEI seito;  <---  struct gakusei seito;と書くこともできる。
GAKUSEI *p;     <---  struct gakusei *p;と書くこともできる。
p = &seito;     <---  これで構造体のポインタ変数pに構造体型
                      の変数seitoの先頭アドレスが代入される。

ポインタ変数から、それによって指されている構造体変数のメンバを参照するには、アロー演算子(->)を用います。

使用例
....
seito.kokugo =100;   <--- これは今までの参照方法(ドット演算子)
p->kokugo = 100;     <--- ポインタ変数をもとに参照する場合はこうなる。 

さらに、通常のポインタ変数と同じように配列を指し示すことも可能です。

使用例
....
GAKUSEI seito[10];
GAKUSEI *p;
p = seito;   <--- 配列名は配列の先頭アドレスですから&は不要。
p->kokugo;   <--- seito[0]のメンバkokugoを指す


演習問題4-4

演習問題4-2を構造体型のポインタ変数を用いて実現せよ。

演習問題4-4の解答


■ 4-8 構造体型の変数に使用可能な演算

構造体型の変数は通常の変数と同じように宣言可能ですが、演算に関しては制限があります。例えば、変数同士の四則演算は行えません。以下に構造体型の変数に対して使用可能な演算について説明します。

変数同士のコピー (=)
GAKUSEI x, y, z[10];
......
x = y;     <---すべてのメンバをコピーしてくれる。
x = z[3];
アドレスを得る (&)
GAKUSEI x, y, z[10];
GAKUSEI *p1, *p2;
......
p1 = &y;
p2 = z;    <--- 配列名はその先頭アドレス。
メンバの参照 (.と->)
GAKUSEI x, y, z[10];
GAKUSEI *p1, *p2;
......
p1 = &y;
p2 = z;
x.kokugo = 100;
p1->suugaku = 20;
p2->eigo = 20;


■ 4-9 構造体と関数

構造体型の変数は通常の変数と全く同様に引数として関数に渡すことが可能ですし、関数の返り値としても利用可能です。特に、関数は通常は一つの値しか返り値として返すことが出来ませんが、複数の値を一つの構造体としてまとめることにより、事実上任意の数の値を返すことが可能となります。

関数の引数や帰りとして構造体を利用するためには、構造体の定義を関数のプロトタイプ宣言の前に記述しておくことが必要となります。

使用例
#include <stdio.h>

typedef struct gakusei {  <--- GAKUSEI型は下記のプロトタイプ宣言
  char name[21];               よりも前に記述しておく必要がある。
  int kokugo; 
  int suugaku;
  int rika;
  int syakai;
  int eigo;
  int max;
  int min;
  float heikin;
} GAKUSEI;

void function1(GAKUSEI x);  <--- 関数のプロトタイプ宣言
GAKUSEI function2(int y);

このように、構造体型の宣言を前もって行う以外は、構造体型であることによる特別な記述等は存在しません。通常の変数、配列などと全く同じです。以降では、演習問題4-5で構造体型変数の値渡しについて、演習問題4-6でアドレス渡しについて、その例を示しておきます。

演習問題4-5

以下の仕様を満たすプログラムを作成せよ。

  1. 氏名(最大20バイト)、5教科の各点数、平均点、最高点、最低点をメンバに持つ構造体gakuseiを宣言し、それをGAKUSEI型として定義している。
  2. キーボードから氏名、5教科の点数の入力を受け付け、平均点を求める関数input_dataを持つ。
  3. 最高点を計算する関数get_max、最低点を計算する関数get_minを持つ。
  4. 氏名、5科目の点数、5教科の平均、5教科のうちの最高点と最低点を表示する関数print_structを持つ。
  5. 上記の関数は,返り値や引数に構造体gakusei型の変数や配列をとる。

演習問題4-5の解答


演習問題4-6

以下の仕様を満たすプログラムを作成せよ。

  1. 氏名(最大20バイト)、5教科の各点数、平均点、最高点、最低点をメンバに持つ構造体gakuseiを宣言し、それをGAKUSEI型として定義している。
  2. キーボードから最大20名分の氏名、5教科の点数の入力を受け付ける関数input_dataを持つ。但し、氏名に"Q"と入力した場合は学生データの入力が終了したものとする。
  3. 入力された学生の情報を5教科の平均の高い順に並び替え、その結果を表示する関数narabikaeを持つ。

演習問題4-6の解答