ほぷしぃ

納得C言語!

[第14回]構造体

構造体


1.構造体とは?

C言語では、複数のデータ型を複数格納することができる箱を作ることができます。
配列で格納できるのは単一データ型のみ(char型を宣言するとchar型以外は格納できない)ですが、これから学習する構造体を使うことでバラバラのデータ型を持った変数を1つにまとめることができます。
例えば、学生名簿を作る時に氏名はchar型、学生番号や学年、クラスはint型という具合に異なったデータ型をまとめることによって分かりやすいプログラムを書くことができます。


2.構造体の作り方

(1)構造体の型を作る

構造体を作るには、まず使うデータ型と名前を宣言した型を用意する必要があります。
構造体の型の作り方は以下の通りです。

構造体の型の作り方

構造体の型を宣言する時は"struct"と言うものを用います。
"struct"を付けることによって、コンパイラに「この部分は構造体の型宣言ですよ」というのを教えます。

この状態では、タグ名の型を作っただけなので、中身はまだ空のままです。
「メンバ名」という変わった表現をしていますが、変数名となんら変わりありません。
ちなみに、最後の } の後に;を付けることをお忘れなく。
さらに、型を置く場所によって有効範囲が変わります。
関数の外で宣言するとグローバル変数のようにプログラム全体で使用することができ、関数の中で宣言するとローカル変数のように、その関数内のみでしか使用できません。


(2)構造体の宣言

(1)を作り終えたら、今度は実際に構造体を宣言して構造体を使えるようにします。
構造体を宣言する方法は2パターンあり、変数を用いて宣言する方法と配列を使って宣言する方法があります。

変数を用いる場合

変数を用いる場合は(変数名)の構造体を1個作成します。

配列を用いる場合

配列を用いる場合は(変数名)の構造体を(要素数)個作成します。


(3)構造体の初期化

構造体は(2)の時に普通の変数同様、初期化することができます。
変数を用いる時と配列を用いる時では宣言方法が若干違います。

構造体の初期化

データ数nは(1)で宣言したメンバ数だけ初期化します。

データ数nは(1)で宣言したメンバ数だけ初期化

データ数nは(1)で宣言したメンバ数だけ初期化し、データ数mは要素数の分だけ初期化します。
要素分のところを空白にしておくと、初期化した数が自動で割り当てられます。
複数宣言する時は1行に並べるのではなく、例のように複数行使って書くと良いでしょう。
そうすると、1行に並べるより断然見栄えが良くなります。
データのところはインデントを使ってさらに見栄えを良くするとGood!

初期化で注意するところは、構造体の型を作ったときにデータ名とメンバ名を定義しました。
データを初期化する時は、定義した順番に初期化してください。

//構造体の型宣言
struct student{
    int no; // 学籍番号
    char name[256]; // 氏名
    int year; // 学年
    char student_class[256]; // クラス
};

//構造体の宣言と初期化の代入
struct student student[200] = {
    {学籍番号, 氏名, 学年, クラス},
    {学籍番号, 氏名, 学年, クラス},
    {学籍番号, 氏名, 学年, クラス},
    {学籍番号, 学年, 氏名, クラス}    //この行はエラーになります
}

上の例では「学籍番号、氏名、学年、クラス」の順で型を宣言しています。ここにデータを格納するときも「学籍番号、氏名、学年、クラス」の順で格納してあげなければいけません。
なので、初期化の代入の最後の行はエラーになってしまいます。


(4)構造体のデータ参照

構造体に格納したデータを変数に代入する時には以下のように参照します。

構造体のデータ参照

構造体変数名とメンバ名の間にピリオドがあります。
このピリオドのことをドット演算子と呼び、構造体を参照する場合に使います。

長々と構造体の作り方について説明してきましたが、例題を書いていきます。
上の説明と照らし合わせて構造体の仕組みを学習していきましょう!

例題1 構造体を作ろう

#include <stdio.h>

//構造体の型宣言
struct OLD
{
    int no;        //番号
    char *name;    //名前
    int s_year;    //年
    char s_class;  //クラス
};

int main()
{
    int i = 0;
    printf("学籍番号\t 名前\t学年\tクラス\n");

    //構造体の初期化
    struct OLD old[15] =
    {
        { 1,"上杉謙信"    ,3,'A'},
        { 2,"武田信玄"    ,3,'A'},
        { 3,"豊臣秀吉"    ,3,'A'},
        { 4,"明智光秀"    ,3,'A'},
        { 5,"織田信長"    ,3,'A'},
        { 6,"徳川家康"    ,3,'A'},
        { 7,"聖徳太子"    ,3,'A'},
        { 8,"マッカーサー",3,'A'},
        { 9,"ザビエル"    ,3,'A'},
        {10,"北条政子"    ,3,'A'},
        {11,"沖田総司"    ,3,'A'},
        {12,"永倉新八"    ,3,'A'},
        {13,"斉藤一"      ,3,'A'},
        {14,"松原忠治"    ,3,'A'},
        {15,"武田観柳斎"  ,3,'A'},
    };

        for(i = 0; i < 15; i++) {
        //結果の出力
        printf("%7d%15s%5d%10c\n", old[i].no, old[i].name, old[i].s_year, old[i].s_class);
    }
    return 0;
}

結果

結果

今回の例題では構造体の配列を宣言しています。
ここまでやったことが理解できれば、簡単な構造体なら作れます。


3.構造体とポインタ

構造体変数も普通の変数と同じようにポインタを使うことができます。

B「構造体変数でもポインタを使うことができるよ」
A「普通の変数と同じように?」
B「ただ少しだけ違うところもあるよ。普通のポインタではアドレスの中身を表示する時に『*』を付けたけど、構造体の場合は『*』はいらないんだ」
A「ふーん」
B「そのかわり、ポインタだけでは各メンバ変数の値を見ることはできないよね?」
A「ん...何で?」
B「構造体のポインタは1つのデータ群の先頭アドレスを指していて、メンバ変数を指しているわけじゃないんだ」
A「あ、、、そうか」
B「これから、ポインタを使った参照方法を勉強していくよ〜」
A「もういやだ・・・」
B「・・・がんばれ」

(1)ポインタを用いた構造体の宣言

ポインタを用いて構造体の宣言を行う方法は以下の通りです。

ポインタを用いた構造体の宣

構造体変数名にポインタの時と同様に*が付きました。
これで宣言された構造体変数はポインタ構造体変数になりました。
書き方は普通のポインタと殆ど変わりませんね。


(2)ポインタ構造体変数に値を代入

ポインタ構造体変数もポインタ変数と同様、変数に値を入れることができます。

ポインタ構造体変数に値を代

これも普通のポインタを変わりません。
但し、構造体変数のアドレスをポインタ構造体変数に代入する時は少し注意が必要です。

ポインタ構造体変数に値を代

(a)は変数を用いて構造体を宣言した時に使います。
構造体は変数を用いて宣言したときは、普通の変数を宣言した時と同じように1つの構造体変数として扱われます。
この時に構造体変数をprintf( )などで出力すると、構造体の型の最初に定義したメンバの値が出力されます。
よってポインタではないので、&を付けてアドレスを意味するようにします。


(b)は配列を用いて構造体を宣言した時に使います。
構造体を配列を用いて宣言したときは、普通の配列を宣言した時と同じように構造体配列として扱われます。
第13回でも学習したとおり、配列で宣言した変数は配列の先頭アドレスを指します。
よって既にポインタであるので&を付ける必要はありません。


(3)ポインタ構造体のデータ参照

ポインタ構造体に格納したデータを変数に代入する時には以下のように参照します。

ポインタ構造体のデータ参照

普通の構造体では構造体変数名とメンバ名の間にピリオドがありましたがポインタ構造体の場合は、ポインタ構造体変数名とメンバ名の間に->があります。
この->をアロー演算子と呼び、ポインタ構造体を参照する場合に使います。
普通の構造体の場合はピリオド(.)、ポインタ構造体の場合はアロー(->)を使うのでしっかりと区別を付けておきましょう。
さらにポインタ構造体変数は第13回で学習したポインタ変数のアドレス計算が可能です。
これでポインタを使った構造体も理解できたはずです。
最後に例題を見ていきましょう。

例題2 アロー演算子を用いる

#include <stdio.h>

//構造体の型宣言
struct OLD
{
    int no;        //番号
    char *name;    //名前
    int s_year;    //年
    char s_class;  //クラス
};

int main()
{
    int i = 0;
    printf("学籍番号\t 名前\t学年\tクラス\n");

    //構造体の初期化
    struct OLD old[15] =
    {
        { 1,"上杉謙信"    ,3,'A'},
        { 2,"武田信玄"    ,3,'A'},
        { 3,"豊臣秀吉"    ,3,'A'},
        { 4,"明智光秀"    ,3,'A'},
        { 5,"織田信長"    ,3,'A'},
        { 6,"徳川家康"    ,3,'A'},
        { 7,"聖徳太子"    ,3,'A'},
        { 8,"マッカーサー",3,'A'},
        { 9,"ザビエル"    ,3,'A'},
        {10,"北条政子"    ,3,'A'},
        {11,"沖田総司"    ,3,'A'},
        {12,"永倉新八"    ,3,'A'},
        {13,"斉藤一"      ,3,'A'},
        {14,"松原忠治"    ,3,'A'},
        {15,"武田観柳斎"  ,3,'A'},
    };

    struct OLD *p;    //構造体のポインタ宣言
    p = old;          //ポインタpに構造体oldの先頭アドレスを代入

    for(i = 0; i < 15; i++){
        //結果の出力
        printf("%7d%15s%5d%10c\n", (p+i)->no, (p+i)->name, (p+i)->s_year, (p+i)->s_class);
    }
    return 0;
}

結果

結果
例題1のプログラムをポインタを用いてみました。
ポインタを用いる方法は色々と便利なことがあるのでここでしっかりと覚えよう。


4.関数と構造体

構造体は関数の引数としても利用することが出来ます。

関数と構造体

このようにして構造体を関数に渡します
普通の関数同様、戻り値を持つ方法と持たない方法の2つがあります。
戻り値を持たない場合は自作関数の引数を構造体で宣言し、戻り値を持つ場合は自作関数の戻り値の型を構造体で宣言してください。
基本的に&を付けない構造体変数の場合は値渡し、&をつけた構造体変数とポインタ構造体変数の場合は参照渡しになります。

ここで、第13回で軽く触れた値渡しと参照渡しについて説明したいと思います。
値渡しはmain関数や自作関数で宣言された変数の値を他の自作関数へ値ごとコピーして渡す方法です。
参照渡しはmain関数や自作関数で宣言された値を他の自作関数へ値が格納されているアドレスをコピーして渡す方法です。

値渡し
参照渡し

変数1つ渡すだけでは値渡しと参照渡しはさほど変わりはありませんが、これが2倍、3倍、さらにはとてつもない膨大な量の配列や構造体になるとどうなるでしょうか。
値渡しは全ての値をコピーして関数に渡すので、コピーする時間が生じ、処理が遅くなります。
一方、参照渡しは配列や構造体の先頭アドレスを渡すだけで良いので、値渡しに比べて格段に処理が早く、メモリ消費も少なくなります。

値渡し
参照渡し

さて、実際に値渡しと参照渡しの例を見てみましょう

例題3 値渡し

#include <stdio.h>

//構造体の引数を持った自作関数の宣言
void output(struct OLD old[]);
//構造体の型宣言
struct OLD
{
    int no;        //番号
    char *name;    //名前
    int s_year;    //年
    char s_class;  //クラス
};

int main()
{
    printf("学籍番号\t 名前\t学年\tクラス\n");
    //構造体の初期化
    struct OLD old[15] =
    {
        { 1,"上杉謙信"    ,3,'A'},
        { 2,"武田信玄"    ,3,'A'},
        { 3,"豊臣秀吉"    ,3,'A'},
        { 4,"明智光秀"    ,3,'A'},
        { 5,"織田信長"    ,3,'A'},
        { 6,"徳川家康"    ,3,'A'},
        { 7,"聖徳太子"    ,3,'A'},
        { 8,"マッカーサー",3,'A'},
        { 9,"ザビエル"    ,3,'A'},
        {10,"北条政子"    ,3,'A'},
        {11,"沖田総司"    ,3,'A'},
        {12,"永倉新八"    ,3,'A'},
        {13,"斉藤一"      ,3,'A'},
        {14,"松原忠治"    ,3,'A'},
        {15,"武田観柳斎"  ,3,'A'},
    };
    //output()関数へ構造体oldを値渡しする
    output(old);

    return 0;
}

//引数に構造体を用いた自作関数output
void output(struct OLD old[])
{
    int i = 0;
    for(i = 0; i < 15; i++){
        //結果の出力
        printf("%7d%15s%5d%10c\n", old[i].no, old[i].name, old[i].s_year, old[i].s_class);
    }
}

例題4 参照渡し

#include <stdio.h>

//構造体の引数を持った自作関数の宣言
void output(struct OLD old[]);
//構造体の型宣言
struct OLD
{
    int no;        //番号
    char *name;    //名前
    int s_year;    //年
    char s_class;  //クラス
};

int main()
{
    printf("学籍番号\t 名前\t学年\tクラス\n");
    //構造体の初期化
    struct OLD old[15] =
    {
        { 1,"上杉謙信"    ,3,'A'},
        { 2,"武田信玄"    ,3,'A'},
        { 3,"豊臣秀吉"    ,3,'A'},
        { 4,"明智光秀"    ,3,'A'},
        { 5,"織田信長"    ,3,'A'},
        { 6,"徳川家康"    ,3,'A'},
        { 7,"聖徳太子"    ,3,'A'},
        { 8,"マッカーサー",3,'A'},
        { 9,"ザビエル"    ,3,'A'},
        {10,"北条政子"    ,3,'A'},
        {11,"沖田総司"    ,3,'A'},
        {12,"永倉新八"    ,3,'A'},
        {13,"斉藤一"      ,3,'A'},
        {14,"松原忠治"    ,3,'A'},
        {15,"武田観柳斎"  ,3,'A'},
    };
    //output()関数へ構造体oldを値渡しする
    output(old);

    return 0;
}

//引数にポインタ構造体を用いた自作関数output
void output(struct OLD *p)
{
    int i = 0;
    for(i = 0; i < 15; i++){
        //結果の出力
        printf("%7d%15s%5d%10c\n", (p+i)->no, (p+i)->name, (p+i)->s_year, (p+i)->s_class);
    }
}

結果

結果

両方とも結果は一緒になります。
例題の場合は構造体の配列を渡しているので、値渡しを使うより参照渡しを使ったほうがメモリ消費を少なくすることができ処理速度が速くなります。


5.練習問題

(1) 下の表を構造体に格納して表示させてみよう。
 氏名は各自ご自由に。

練習問題

(2) (1)で作ったプログラムの表示部分を自作関数にして表示させてみよう。

[第13回]ポインタ ページのトップ 解答