構造体

構造体を使うと、いくつかの異なったタイプのデータをひとかたまりのものとして扱えます。 ここでは構造体についての基本的なことを学びます。 感覚的には「既存の型の組み合わせを使って、自分好みの新しい構造を持った型を作る。」というような感じで捉えておけば良いと思います。

次回の講義では「ファイル構造体」というものを習います。 また、後期の終わりに C 言語を拡張した C++ 言語を少しやります。 そこではここで学習する構造体を拡張したクラスというものが出てきます。 更に応用プログラミングIIの方の JAVA 言語でもクラスというものが出てきます。 クラスに関する理解の準備のためにも構造体をしっかり身につけて下さい。

構造体の定義

いくつかの異なったタイプのデータをひとかたまりにして構造体(structure)を構成します。 例えば「学生証番号(整数データ)」と「名前(文字列データ)」と「英語の点数(整数データ)」をひとかたまりにした構造体を seisekiTag という名前で定義するときには次のように

    struct seisekiTag {
        int ID;
        char name[64];
        int english;
    };
と書きます。(一番最後のセミコロン、忘れがちになるので注意) 一般には構造体タグに好きな名前をつけて、好きなだけメンバを登録して、次のように構造体を定義することが出来ます。
    struct 構造体タグ {
        型 メンバ1;
        型 メンバ2;
        ・・・・
    };

構造体変数の宣言

定義した構造体タグを使って構造体変数を次のように宣言します。

    struct seisekiTag seiseki1, seiseki2, seiseki3;
これで seisekiTag で定義された構造を持った変数 seiseki1 と seiseki2 と seiseki3 を宣言(メモリのどこかに変数を格納する領域を確保)したことになります。 seisekiTag で定義された構造を持った変数のことを「struct seisekiTag 型変数」と言うこともあります。

一般的には次のように宣言します。

    struct 構造体タグ 構造体変数1, 構造体変数2, ・・・・;

また、次のようにすれば構造体の定義と変数の宣言を同時に行うこともできます。

    struct seisekiTag {
        int ID;
        char name[64];
        int english;
    } seiseki1, seiseki2, seiseki3;
この場合は構造体タグ (seisekiTag の部分) を省略しても構いません。

ドット演算子とメンバへのアクセス

構造体のメンバにアクセスする際にはドット演算子「.」を使います。

    構造体変数名.メンバ名
で構造体変数の各メンバにアクセス(値を格納したり、格納されてる値を取り出したり)できます。

例えば構造体変数 struct1 のメンバの ID に数値 1 を代入するときは次のように書きます。

    seiseki1.ID = 1;
struct1 のメンバ name に文字列 "金沢太郎" を代入するときは標準ライブラリ関数の strcpy を使って
    strcpy(seiseki1.name, "金沢太郎");
とします。 seiseki.name = "金沢太郎"; とすることは出来ません。 (name を文字列ではなくポインタで宣言してれば出来ますが、ややこしくなりそうなので説明は省略します。)

構造体変数の初期化

構造体変数は宣言と同時に初期化することが出来ます。 例えば

    struct seisekiTag seiseki1 = {1, "金沢太郎", 80};
とすると seiseki1 の1番目のメンバ ID に 1 を、2番目のメンバ name に "金沢太郎" を、3番目メンバ english に 80 を初期値として与えることになります。

構造体の代入

同じ定義の構造体同士なら次の式で全てのメンバを代入(コピー)することが出来ます。

    構造体変数1 = 構造体変数2;
上の式で構造体変数2の各メンバの全ての値を構造体変数1に代入することになります。

例えば

    seiseki1 = seiseki2;
とするのは
    seiseki1.ID = seiseki2.ID;
    strcpy(seiseki1.name, seiseki2.name);
    seiseki1.english = seiseki2.english;
と書くのと同じことになります。

構造体を使ったサンプルプログラムです。

例:ex0901.c
 1 #include <stdio.h>
 2 #include <string.h>
 3 void main() {
 4     struct seisekiTag {
 5         int ID;
 6         char name[64];
 7         int english;
 8     };
 9     struct seisekiTag seiseki1, seiseki2;
10     seiseki1.ID = 1;
11     strcpy(seiseki1.name, "金沢太郎");
12     seiseki1.english = 80;
13     printf("成績1\n");
14     printf("ID=%d\n", seiseki1.ID);
15     printf("名前=%s\n", seiseki1.name);
16     printf("英語点数=%d\n\n", seiseki1.english);
17     seiseki2 = seiseki1;
18     printf("成績2\n");
19     printf("ID=%d\n", seiseki2.ID);
20     printf("名前=%s\n", seiseki2.name);
21     printf("英語点数=%d\n", seiseki2.english);
22 }
出力結果
成績1
ID=1
名前=金沢太郎
英語点数=80

成績2
ID=1
名前=金沢太郎
英語点数=80

構造体の配列

構造体変数の配列を宣言したりもできます。 例えば seiseki[0]seiseki[1]seiseki[2] という要素数3の構造体変数配列を宣言したいときは

    struct seisekiTag seiseki[3];
と宣言します。

各要素のメンバにはドット演算子を使ってアクセスできます。 例えば

    seiseki[2].ID
とするとstruct seisekiTag型の配列 seiseki の3番目の要素 seiseki[2] のメンバ名 ID にアクセスできます。

構造体へのポインタ

構造体へのポインタの宣言は

    struct seisekiTag *seiseki_ptr;
と宣言します。 ポインタ変数 seiseki_ptr には struct seisekiTag 型の値を格納するメモリのアドレスを代入します。

間接参照演算子を使ってアドレスの先を参照します。 本文で *seiseki_ptr と書くことで参照先の構造体を操作することが出来ます。 例えば

    (*seiseki_ptr).ID
で参照先のメンバ名IDにアクセスできます。 また、(*seiseki_ptr).ID の命令と全く同じ命令をアロー演算子「->」を使って
    seiseki_ptr->ID
と書くこともできます。

構造体へのポインタの話まで行くとややこしくなるのでこれくらいにしておきますが、単に構造体の考え方とポインタの考え方を組み合わせただけなので、じっくり考えれば理解できます。

typedef 指定子

これまでの例では構造体を宣言するときに struct studentTag と少し長たらしい書き方になってました。 例題では main 関数のみで数回しか宣言しないのでそれでも良かったのですが、複数の関数で同じ定義の構造体を使うときに何度も宣言する場合は面倒臭くなってきます。 ここでは typedef 指定子を利用して構造体に新しい型名(別名)をつけて扱う方法を説明します。

typedef は読んで字のごとく型名(type name)を定義(define)するという意味です。 次のように使います。

typedef 別名をつけたい型 別名;
先の例の学生の英語の成績の構造体に別名 SEISEKI をつけたい場合は次のようにします。
struct seisekiTag {
    int ID;
    char name[64];
    int english;
};
typedef struct seisekiTag SEISEKI;
最後の typedef struct seisekiTag SEISEKI;struct seisekiTag という型の別名 SEISEKI という型名を定義してます。 (別名は予約語でなければ大文字小文字どちらでも使えますが、構造体の別名は慣習的に大文字を使っていることが多いです。)
同じことを1文で行ったのが次の例です。
typedef struct {
    int ID;
    char name[64];
    int english;
} SEISEKI;
これで ID, namem, english の3つのメンバを持つ構造体に別名 SEISEKI をつけたことになります。

サンプルプログラムです。

例:ex0901_td.c
 1 #include <stdio.h>
 2 #include <string.h>
 3 typedef struct {
 4     int ID;
 5     char name[64];
 6     int english;
 7 } SEISEKI;
 8 void main() {
 9     SEISEKI seiseki1, seiseki2;
10     seiseki1.ID = 1;
11     strcpy(seiseki1.name, "金沢太郎");
12     seiseki1.english = 80;
13     printf("成績1\n");
14     printf("ID=%d\n", seiseki1.ID);
15     printf("名前=%s\n", seiseki1.name);
16     printf("英語点数=%d\n\n", seiseki1.english);
17     seiseki2 = seiseki1;
18     printf("成績2\n");
19     printf("ID=%d\n", seiseki2.ID);
20     printf("名前=%s\n", seiseki2.name);
21     printf("英語点数=%d\n", seiseki2.english);
22 }
出力結果
成績1
ID=1
名前=金沢太郎
英語点数=80

成績2
ID=1
名前=金沢太郎
英語点数=80
この例ではあまり意味がありませんが 3~7行目の main 関数の外で別名をつけることで同じファイル内の別の関数でも SEISEKI という型名を使って構造体の宣言ができるようになります。 9行目で型名 SEISEKI を使って構造体変数 seiseki1 と seiseki2 を宣言してます。

構造体の入れ子

構造体を構造体のメンバにすることができます。 構造体の中に構造体が入れ子のように入っている状態です。 入れ子の事をネスト(nest)とも言います。

以下の例は英語、国語、数学の点数で構造体 EXAM を定義し、 それを 構造体 SEISEKI のメンバに組み込んでます。 構造体の中の構造体のメンバにはドット演算子を2回使ってアクセスします。

例:stnest_seiseki.c
 1 #include <stdio.h>
 2 typedef struct {
 3     int english;
 4     int japanese;
 5     int math;
 6 } EXAM;
 7 typedef struct {
 8     int ID;
 9     char name[64];
10     EXAM siken;
11 } SEISEKI;
12 void main() {
13     SEISEKI seiseki1 = {1, "金沢太郎", {80, 70, 75}};
14     SEISEKI seiseki2 = {2, "石川次郎", {60, 50, 45}};
15     printf("成績1\n");
16     printf("ID=%d\n", seiseki1.ID);
17     printf("名前=%s\n", seiseki1.name);
18     printf("英語点数=%d\n", seiseki1.siken.english);
19     printf("国語点数=%d\n", seiseki1.siken.japanese);
20     printf("数学点数=%d\n", seiseki1.siken.math);
21     printf("\n");
22     printf("成績2\n");
23     printf("ID=%d\n", seiseki2.ID);
24     printf("名前=%s\n", seiseki2.name);
25     printf("英語点数=%d\n", seiseki2.siken.english);
26     printf("国語点数=%d\n", seiseki2.siken.japanese);
27     printf("数学点数=%d\n", seiseki2.siken.math);
28 }
出力結果
成績1
ID=1
名前=金沢太郎
英語点数=80
国語点数=70
数学点数=75

成績2
ID=2
名前=石川次郎
英語点数=60
国語点数=50
数学点数=45