構造体

プログラミングBのページへ戻る

その前に, typedef

typedef を用いると,既に定義されている型に,別の新しい名前をつけて定義することができる(typedef は型(type)定義(definition)を略したキーワードである).その方法は次の通り.

typedef 定義されている型 定義する新しい型名;

具体的には次のようにする.

typedef int seisu_t;

これは, seisu_t という名前の型は int 型であると定義している. この型定義をした後では, seisu_t を int の代わりに使うことができる. もちろん,int も以前と同様に使うことができる.

typedef で定義した型名には _t を付ける慣習がある. この慣習に従って _t を付けておくと, それが typedef で定義された型名であることが一目で分かるので便利である.

typedef int seisu_t;

main()
{
    int a;        /* これは整数型変数の宣言 */
    seisu_t b;      /* これも整数型変数の宣言 */
}

上の例は,あまりにも馬鹿げているので,もう少し意味のある例として,次のようなものも挙げておこう.

typedef int * intPtr_t;

これは, intPtr_t という名前の型は int型を指すポインタ型であると定義している.

typedef で定義した新しい名前の型は,変数宣言の他にも,普通の型と同じように使える.

typedef int * intPtr_t;

/* intPtr_t 型を引数にとり intPtr_t 型を戻り値とする関数 */
/* p のポインタ値を 3 だけ進めたポインタ値を返す */
intPtr_t foo(intPtr_t p)     
{
    return p + 3;
}

main()
{
    int a[10];
    intPtr_t p;           /* intPtr_t 型の変数宣言 */
    
    p = foo(a);           /* p の値は a[3] を指すポインタ値 */
}

ページ先頭に戻る

構造体とは

 構造体とは,「いろいろな種類の互いに関連するデータをまとめて, 1つのかたまりにしたもの」である。 たとえば,「氏名,年齢,性別などのデータを一人分だけまとめたもの」 このようなものをいう。

構造体

 構造体を構成する要素を,構造体のメンバと呼ぶ。上の例では, 「名前」「性別」「年齢」「身長」「体重」などが,メンバにあたる。

ページ先頭に戻る

構造体の宣言

 構造体は,一つのデータ型であり,その型枠をまず始めに宣言する必要がある。 そして,その型枠を型とする変数を宣言する形で構造体の実体(オブジェクト)を宣言し, それを使用することができる。

構造体の型枠の宣言と,その型枠をもつ構造体変数の宣言は次のようになされる.

struct 構造体タグ名 {メンバの並び};   /* 型枠の宣言 */

struct 構造体タグ名 構造体変数名;     /* 構造体変数の宣言 */

例えば,次図のような人のデータをまとめた構造体の定義は下のようになる。

person 構造体
struct _person {           /* _person がタグ名 */
    char name[20];        /* 文字配列型のメンバ name */
    char sex;              /* 文字型メンバ sex */
    int age;               /* 整数型メンバ age */
    double height;         /* 倍精度実数型メンバ height */
    double weight;         /* 倍精度実数型メンバ weight */
};


struct _person p;      /* p という名前の struct _person 型変数を宣言 */

この構造体のタグ名は _person である. タグ名にはその先頭に _ をつける慣習があるので,それに従って _person としたが, _ は必ずしも必要ではない。

 構造体タグ名は型名ではなく,常に struct というキーワードを必要とする。つまり,単独では意味をなさず,次のような書き方はできない。

_person p;           /* 間違い */

これが面倒なときは, typedef を用いて, struct _person を別の名前の型, 例えば person_t として定義しておけばよい。そうすれば,person_t はそのままで型名であるから, struct を付ける必要がない。

typedef struct _person person_t;  /* struct _person を person_t という型名で定義 */

person_t p;              /* 変数名 p の person_t 型変数を宣言 */

 このように, typedef を用いて新しい型名を定義するのならば,はじめから次のように書いても良い。また,この場合には,構造体タグ名は必要ではない。

typedef struct {         /* 構造体の型枠を定義して,同時にそれを型名 person_t として定義する */
    char name[20];
    char sex;
    int age;
    double height; 
    double weight; 
} person_t;

person_t p;             /* 変数名 p の person_t 型変数を宣言 */

 以後は,この typedef を用いて新しい型名を直接に定義する方法で構造体型の宣言を行うこととする。

初期値設定

 構造体型の変数を宣言するのと同時に,その各メンバの初期値を設定することができる。各メンバの初期値は { } で括られ,構造体の定義にある順番に各メンバの値となる。

person_t p = {"Tom", 'M', 20, 175.2, 66.5};

ページ先頭に戻る

メンバ参照(直接参照)

 構造体型変数の中にあるメンバを参照(アクセス)するには, 変数名の後に "." (ピリオド)に続いてメンバ名を付ける。 たとえば,先ほどの例で用いた p という変数内の age を参照するには, p.age とする。次の例では, p 内のメンバ age に 30 を代入している。

typedef struct {
    ....
    int age;              /* 構造体のメンバの一つ */
    ....
} person_t;

int main()
{
    person_t p;           /* person_t 型変数 p の宣言 */

    p.age = 30;         /* p 内のメンバ age に 30 を代入 */
    
    ....

この "." は,ドット演算子あるいはメンバ参照演算子と呼ばれる.

ページ先頭に戻る

サンプルプログラム1

#include <stdio.h>

typedef struct {   
    char name[20];
    char sex; 
    int age;
    double height; 
    double weight; 
} person_t;

main()
{
    person_t p = {"Tom", 'M', 19, 175.2, 69.5};
    
    printf("%s %c %d %f %f\n", p.name, p.sex, p.age, p.height, p.weight);
    p.age ++;
    p.height += 0.7;
    p.weight -= 1.5;
    printf("%s %c %d %f %f\n", p.name, p.sex, p.age, p.height, p.weight);

    return 0;
}
Tom M 19 175.200000 69.500000
Tom M 20 175.900000 68.000000

ページ先頭に戻る

構造体の代入

一つの構造体変数の内容全部を 同じ型の別の構造体変数に, 通常の変数を代入するのと同じように,代入することができる.

    person_t p1 = {"Tom", 'M', 19, 175.2, 69.5};
    person_t p2;
    
    p2 = p1;
構造体の代入
#include <stdio.h>

typedef struct {   
    char name[20];
    char sex; 
    int age;
    double height; 
    double weight; 
} person_t;

main()
{
    person_t p1 = {"Tom", 'M', 19, 175.2, 69.5};
    person_t p2;
    
    p2 = p1;         /* 構造体の代入 */
    
    printf("%s %c %d %f %f\n", p2.name, p2.sex, p2.age, p2.height, p2.weight);

    return 0;
}
Tom M 19 175.200000 69.500000

ページ先頭に戻る

構造体のサイズ

構造体のもつメモリサイズは,他の型や変数と同じように, sizeof 演算子を用いて得られる.

#include <stdio.h>

typedef struct {   
    char name[20];
    char sex;
    int age;
    double height;
    double weight;
} person_t;

main()
{
  person_t p;

  printf("name=%d, sex=%d, age=%d, height=%d, weight=%d\n",
         sizeof(p.name), sizeof(p.sex), sizeof(p.age), sizeof(p.height), sizeof(p.weight));
  printf("person_t=%d\n", sizeof(p));
}

上のプログラムを実行すると,構造体型である person_t 型のメモリサイズが表示される.

name=20, sex=1, age=4, height=8, weight=8
person_t=44

person_t 構造体のメンバのメモリサイズを全て足し合わせると 41 であるのに, person_t 型の変数のメモリサイズは 44 となっている. その理由は,コンピュータのアクセス速度を速めるために, アクセスに都合の良い位置にメンバを配置するためである. その結果,メンバとメンバの間に隙間(パディングと呼ぶ)ができて,全体のメモリサイズが大きくなる.

ページ先頭に戻る

構造体の配列

構造体を並べた配列も扱える.その宣言定義は通常のようにすればよい.

#define PERSON_NUM 5

typedef struct {   
    char name[20];
    char sex; 
    int age;
    double height; 
    double weight; 
} person_t;

person_t p[PERSON_NUM];

これで,要素数がPERSON_NUM個(5個)の person_t 構造体型配列 p ができる.

構造体配列

たとえば,上の図で色のついてある p[3] の height メンバには, p[3].height でアクセスする.

サンプルプログラム2

#include <stdio.h>

#define PERSON_NUM 5

typedef struct {   
    char name[20];
    char sex; 
    int age;
    double height; 
    double weight; 
} person_t;

main()
{
    person_t p[PERSON_NUM] = {{"Bob",      'M', 19, 165.4, 72.5},
                              {"Alice",    'F', 19, 161.7, 44.2},
                              {"Tom",      'M', 20, 175.2, 66.3},
                              {"Stefany",  'F', 18, 159.3, 48.5},
                              {"Leonardo", 'M', 19, 172.8, 67.2}};
    int i;
    double height_sum, weight_sum, height_ave, weight_ave;                          
    
    height_sum = weight_sum = 0.0;
    for (i = 0; i < PERSON_NUM; i++) {
        height_sum += p[i].height;
        weight_sum += p[i].weight;
    }
    height_ave = height_sum / PERSON_NUM;
    weight_ave = weight_sum / PERSON_NUM;
    
    printf("average height = %f\n", height_ave);
    printf("average weight = %f\n", weight_ave);
    
    return 0;
}

ページ先頭に戻る

構造体をメンバにする構造体

構造体のメンバに構造体を含めることもできる.

次の構造体の型 couple_t は,先ほど定義した person_t 構造体型のメンバを2個と,int 型のメンバを1個持っている.

typedef struct {
    person_t boy;   /* カップルのうちの男の子の情報 */
    person_t girl;  /* カップルのうちの女の子の情報 */
    int month;      /* 交際歴(月数) */
} couple_t;
構造体の構造体

その変数宣言と初期値設定の仕方は,例えば次のようになる. 一つの構造体メンバは { } で括られていることに注意しよう.

構造体のメンバである構造体のメンバを参照するには, ドット演算子を続けて使用すればよい. 例えば, couple_t 型変数 cpl のメンバ boy の中のメンバ name を参照するには, cpl.boy.name とすればよい.

main()
{
    couple_t cpl = {{"Tom",     'M', 20, 175.2, 66.3},
                    {"Stefany", 'F', 18, 159.3, 48.5},
                    8};

    printf("%s and %s are going together for %d month(s).\n", 
        cpl.boy.name, cpl.girl.name, cpl.month);

    return 0;
}
Tom and Stefany are going together for 8 month(s).

構造体型であるメンバには,同じ型の構造体を直接代入することができる.

     person_t newboy = {"Leonardo", 'M', 19, 172.8, 67.2};
     
     cpl.boy = newboy;
     cpl.month = 1;
     
    printf("%s and %s are going together for %d month(s).\n", 
        cpl.boy.name, cpl.girl.name, cpl.month);

    return 0;
}     
Leonardo and Stefany are going together for 1 month(s).

ページ先頭に戻る

関数と構造体

解説のためのサンプルとして,複素数を表す構造体を作っておく.

typedef struct {
    double re;    /* 実数部分 */
    double im;    /* 虚数部分 */
} complex_t;

構造体型の関数引数

関数の引数として,構造体全体の全体を(その値をコピーして)引き渡すことができる. (配列では,そのようなことはできなかったことに注意しよう)

次の関数 printComplex は complex_t 構造体型の引数をとり,それを表示する.

void printComplex(complex_t c)
{
    printf("%f + %f i\n", c.re, c.im);
}

main()
{
    complex_t c = {1.2, 3.4};
    
    printComplex(c);
}
1.200000 + 3.400000 i

構造体型の関数戻り値

構造体型を関数の戻り値の型とすることができる. (配列を関数の戻り値とすることはできなかったことに注意しよう)

次の関数 addComplex は complex_t 構造体型の引数を二つとり, その和を complex_t 構造体型として返す.

complex_t addComplex(complex_t a, complex_t b)
{
    complex_t c;
    
    c.re = a.re + b.re;
    c.im = a.im + b.im;
    
    return c;
}

main()
{
    complex_t a = {1.2, 3.4};
    complex_t b = {5.6, 7.8};
    complex_t c;
    
    c = addComplex(a, b);
    printComplex(c);
    
    return 0;
}
6.800000 + 11.200000 i

構造体ポインタ型の関数引数

関数の引数に,構造体へのポインタ値を与えることもある.

次の関数 addComplexPtr は,3個の complex_t ポインタ型を引数にとり, 最初の2つが指すところにある complex_t 型の値を加えたものを, 最後のものが指すところにある complex_t 型に代入する.

void addComplexPtr(complex_t *a, complex_t *b, complex_t *c)
{
    (*c).re = (*a).re + (*b).re;
    (*c).im = (*a).im + (*b).im;
}

main()
{
    complex_t x = {1.2, 3.4};
    complex_t y = {5.6, 7.8};
    complex_t z;
    
    addComplexPtr(&x, &y, &z);
    printComplex(z);
    
    return 0;
}
6.800000 + 11.200000 i

このように,構造体全体の値をコピーして引数に渡すのではなく, ポインタ値だけを渡す理由には,おおよそ2通りある.

一つは,構造体全体の値よりもポインタ値だけの方が引き渡す手間が少いということである. これは,メモリと実行時間の有効利用という点では大事である. この例の場合,最初の2個の引数 a と b とがポインタ値となっている理由がそうである.

もう一つは,関数外部の構造体の中身を関数内部で変更したいときである. この例の場合,最後の引数 c がポインタ値となっている理由がそうである.

次で説明する間接メンバ参照演算子 -> を用いて,次のようにも書くことができる.

void addComplexPtr(complex_t *a, complex_t *b, complex_t *c)
{
    c->re = a->re + b->re;
    c->im = a->im + b->im;
}

ページ先頭に戻る

メンバ参照(間接参照)

 構造体を扱うときに,構造体型変数を直接扱うのではなくて,構造体型変数へのポインタを介して,間接的に扱うことがよくある。 そのようなときのために,構造体型を指すポインタ値からそれが指す構造体のメンバを参照するための演算子 "->" がある。その書き方は次の通り。

構造体を指すポインタ -> 構造体のメンバ名

このように書いた場合,それは次のように書くのと同等である。

(* 構造体を指すポインタ).構造体のメンバ名

 たとえば次の例では,構造体 person_t 型の変数 p のアドレスを, person_t 型を指すポインタ変数 pp に代入し,次に pp が指す構造体(すなわち p)のメンバ age に 45 を代入している。

person_t p;        /* 構造体 person_t 型変数の宣言 */
person_t * pp;     /* person_t 型を指すポインタ型の変数の宣言 */

pp = &p;           /* pp に p のアドレスを入れる */
pp -> age = 45;    /* pp が指す構造体のメンバ age に 45 を代入 */

また,先ほどの関数の例の場合は,引数に与えられた構造体へのポインタ値 a, b, c からの間接参照を用いている.

void addComplexPtr(complex_t *a, complex_t *b, complex_t *c)
{
    c->re = a->re + b->re;
    c->im = a->im + b->im;
}

 構造体のメンバを直接参照するための演算子 "." は, 「メンバ直接参照演算子」 あるいは単に「メンバ参照演算子」あるいは「ドット演算子」などと呼ばれる。
 また,ポインタを介して構造体のメンバを間接参照するための演算子 "->" は, 「メンバ間接参照演算子」あるいは「アロー(矢印)演算子」などと呼ばれる。

ページ先頭に戻る

共用体

構造体とよく似たものに,共用体(union)と呼ばれるものがある. それは,構造体と同じく,いろいろな型のデータをまとめたものであり, その定義の仕方,メンバへの参照の仕方なども構造体と同じである. ただ,違っているのは,そのメンバが同じメモリを共用しているという点である.

したがって,共用体型の一つの変数のメンバたちは,そのうちのどれか一つしか同時には使用できない.

例えば,人のデータ(名前,性別,年齢,...)をまとめたものを共用体として作った場合,その定義は次のようになる.

union _person {           /* _person がタグ名 */
    char name[20];        /* 文字配列型のメンバ name */
    char sex;              /* 文字型メンバ sex */
    int age;               /* 整数型メンバ age */
    double height;         /* 倍精度実数型メンバ height */
    double weight;         /* 倍精度実数型メンバ weight */
};

union _person p;      /* p という名前の union _person 型変数を宣言 */

この共用体型変数 p のメンバは,次の図のように同じメモリ上に重なるように配置される. したがって,共用体全体のメモリサイズは,そのメンバの最大のメモリサイズに一致する. (この union _person の場合は, name が 20 バイトで最大であるから,共用体全体のサイズも 20 となる)

共用体

共用体には,非常に特殊な使い道があるが,ここではその説明はしないでおく.

ページ先頭に戻る

レポート問題

次のように,person_t 構造体で3人分のデータを作り,それを eggx を用いて画像で表示せよ.(楕円の色と大きさと形とには,性別,身長,体重を反映させる.)

楕円の描き方については, プログラミングAのウェブページに書いてある.

一人分の絵を指定した所に表示する関数 displayPerson を作って, それを利用するようにするとなお良い. そのような関数を作るのが難しい人は,すべて main の中で行ってもよい.

typedef struct {   
    char name[20];
    char sex; 
    int age;
    double height; 
    double weight; 
} person_t;


/* win は eggx の window 番号
   x は楕円などを描く x 座標 */
void displayPerson(int win, int x, person_t * psn)
{
    ........
}


main()
{
    person_t p1 = {"Bob", 'M', 19, 165.4, 72.5};
    person_t p2 = {"Alice", 'F', 19, 161.7, 44.2};
    person_t p3 = {"Tom", 'M', 20, 175.2, 66.3};
    
    .......

    return 0;
}
Bob & Alice & Tom

ページ先頭に戻る