C言語編 第26章 構造体

先頭へ戻る

この章の概要

この章の概要です。


構造体

構造体は、1個以上の変数をひと塊にまとめた型です。 構造体に含まれる要素のことを、メンバフィールドと呼びます。

構造体のメンバの型は、それぞれ異なっても構いません。 この点が、配列とは大きく異なります。

なお、配列型と構造体型はいずれも、複数の要素が集まったものという共通点があります。 これらの型を総称して、集成体と呼びます。

まずは、構造体は型であることを強く意識して下さい。 int型、配列型などと同様、構造体型と呼びます。
そのため、構造体を使うには、まず、構造体型がどんなものであるのかを定義する必要があります。 構造体型の定義は、次の構文で行います。

struct タグ名 {
    型 メンバ名;
    型 メンバ名;
      :
};

structキーワードを使います。

「タグ名」には、タグ(構造体タグ)に付ける名前を記述します。 タグとは、複数の構造体型を区別するために使う名前です。 いわば、「int」や「double」のような型名にあたるものです。 定義しようとしている構造体型が何を表現しているものなのかが分かるように、名前を決めます。
なお一般的には、タグ名は先頭の文字を大文字にすることが多いです。 また、名前の末尾に「_tag」のような目印を付けることもあります。

「型 メンバ名」は、いつもの変数宣言と同じです。ただし、初期値は与えられませんし、static などの指定子も付けられません。なお、構造体のメンバは、その構造体に所属するものであって、他の場所にある名前と被っても問題ありません

こうして構造体型を定義しておけば、これを「int」のような型のように使うことができるようになります。 実際のプログラムで確認してみましょう。

#include <stdio.h>
#include <string.h>

#define STUDENT_NAME_LEN 32         /* 生徒の名前データの最大長 */

/* 生徒のデータ */
struct Student_tag {
    char  name[STUDENT_NAME_LEN];   /* 名前 */
    int   grade;                    /* 学年 */
    int   class;                    /* 所属クラス */
    int   score;                    /* 得点 */
};


void printStudentData(struct Student_tag student);

int main(void)
{
    struct Student_tag student;

    strcpy( student.name, "Saitou Takashi" );
    student.grade = 2;
    student.class = 3;
    student.score = 80;

    printStudentData( student );

    return 0;
}

/*
    生徒のデータを出力する。
    引数:
        student: 出力するデータを集めた構造体変数。
*/
void printStudentData(struct Student_tag student)
{
    printf( "name: %s\n", student.name );
    printf( "grade: %d\n", student.grade );
    printf( "class: %d\n", student.class );
    printf( "score: %d\n", student.score );
}

実行結果:

name: Saitou Takashi
grade: 2
class: 3
score: 80

構造体の定義を、関数定義の外側で行っていますが、実際には関数定義の内側に書いても構いません。いずれにしても、構造体型を使おうとしている箇所よりも手前に、構造体の定義がなければなりません。もし、複数のソースファイルで使いたいのなら、構造体の定義をヘッダファイルに記述すれば良いです。

構造体型を実際に使うには、構造体型の変数(構造体変数)を宣言します。考え方は、int型などの変数を宣言する場合と同じですが、構文としては、以下のように structキーワードが必要だという違いがあります。

struct タグ名 変数名;

C++ の場合、structキーワードは省略可能です(Modern C++編【言語解説】第2章

配列型の変数の場合、各要素へのアクセスには添字を使っていました。構造体変数の場合には、「.」で表現されるメンバアクセス演算子を使用します。単に、ドット演算子と呼ぶこともあります。

構造体のメンバへアクセスするために使う演算子には、もう1つ「->」があります(第31章)。こちらもメンバアクセス演算子の一種ですが、区別のために、こちらはアロー演算子と呼ぶことが多いです。

ドット演算子を使ってメンバへアクセスするには、次の構文を使います。

構造体変数.メンバ名;

具体的なコードで書くと、例えば次のようになります。

student.score = 80;
printf( "%d\n", student.score );

なお、構造体変数は、関数に引数で渡せますし、戻り値で返すこともできます。ただし、構造体型はメンバが大量にあると、受け渡すデータ量が大きくなりますから、実行時の処理負荷も大きいです。

処理負荷の大きさを気にするのなら、ポインタを使って効率を上げられます。第33章で取り上げます。

構造体変数の初期化

構造体変数の初期化に関するルールは、配列の場合と似ています(集成体として、共通のルールが適用されます。型の分類については、APPENDIX を参照)。

明示的に初期値を与えなかった場合、自動記憶域期間を持つのなら各メンバは不定値であり、静的記憶域期間を持つのなら、各メンバは暗黙的に初期化されます第22章)。

構造体変数の宣言と同時に初期値を与えるには、次のように書きます。

struct タグ名 変数名 = { 初期化子, … };

{ } の内側に、1つ以上の初期化子をカンマ区切りで並べます。

1つ目の初期化子は1つ目のメンバに、2つ目の初期化子は2つ目のメンバに適用されます。

初期化子の方が、実際のメンバ数よりも少ない場合は、不足したメンバにデフォルトの初期値が与えられます。デフォルトの初期値は、静的記憶域期間の変数に与えられるデフォルトの初期値の規則と同様で、大雑把にいえば 0 です(第22章)。

初期化子の方が多い場合は、コンパイルエラーです。

この構文を使って、最初のサンプルプログラムを書き換えると、次のようになります(main関数だけ抜粋)。

int main(void)
{
    struct Student_tag student = { "Saitou Takashi", 2, 3, 80 };

    printStudentData( student );

    return 0;
}

便利ではありますが、あとからメンバを間に挟み込むようなことをすると、初期値がずれてしまう点には注意が必要です

C99 では、この問題への対処として、要素指示子というものが導入されています。

また、自動記憶域期間を持つ場合は、同じ型の別の構造体変数を使って初期化できます。この場合、元の構造体変数と同じ値を持った状態に初期化されます

void f(void)
{
    struct Student_tag student1 = { "Saitou Takashi", 2, 3, 80 };
    struct Student_tag student2 = student1;
    static struct Student_tag student3 = student1;  /* コンパイルエラー(自動記憶域期間を持たないため)*/
}

構造体型の配列を使いたいときはどうでしょうか。前章から抜粋すると、配列の初期化は次のように行えるのでした。

要素の型 配列名[要素数] = { 0番目の要素の初期値, 1番目の要素の初期値, … };

要素が構造体型になので、「n番目の要素の初期値」の部分に、{ 1つ目のメンバの初期値, 2つ目のメンバの初期値, … } を当てはめます。

#include <stdio.h>
#include <string.h>

#define STUDENT_NAME_LEN 32         /* 生徒の名前データの最大長 */

/* 生徒のデータ */
struct Student_tag {
    char  name[STUDENT_NAME_LEN];   /* 名前 */
    int   grade;                    /* 学年 */
    int   class;                    /* 所属クラス */
    int   score;                    /* 得点 */
};


void printStudentData(struct Student_tag student);

int main(void)
{
    struct Student_tag students[] = {
        { "Saitou Takashi", 2, 3, 80 },
        { "Suzuki Yuji ", 1, 1, 67 },
        { "Itou Miyuki", 3, 1, 79 },
    };
    int i;

    for( i = 0; i < sizeof(students) / sizeof(students[0]); ++i ){
        printStudentData( students[i] );
    }

    return 0;
}

/*
    生徒のデータを出力する。
    引数:
        student: 出力するデータを集めた構造体変数。
*/
void printStudentData(struct Student_tag student)
{
    printf( "name: %s\n", student.name );
    printf( "grade: %d\n", student.grade );
    printf( "class: %d\n", student.class );
    printf( "score: %d\n", student.score );
}

実行結果:

name: Saitou Takashi
grade: 2
class: 3
score: 80
name: Suzuki Yuji
grade: 1
class: 1
score: 67
name: Itou Miyuki
grade: 3
class: 1
score: 79

C99 (要素指示子)

C99 で追加された要素指示子を使って、特定のメンバを選んで初期値を与えられます。

#include <stdio.h>
#include <string.h>

#define STUDENT_NAME_LEN 32         /* 生徒の名前データの最大長 */

/* 生徒のデータ */
struct Student_tag {
    char  name[STUDENT_NAME_LEN];   /* 名前 */
    int   grade;                    /* 学年 */
    int   class;                    /* 所属クラス */
    int   score;                    /* 得点 */
};


void printStudentData(struct Student_tag student);

int main(void)
{
    struct Student_tag student = { 
        .name = "Saitou Takashi",
        .grade = 2,
        .class = 3,
        .score = 80
    };

    strcpy( student.name, "Saitou Takashi" );
    student.grade = 2;
    student.class = 3;
    student.score = 80;

    printStudentData( student );

    return 0;
}

/*
    生徒のデータを出力する。
    引数:
        student: 出力するデータを集めた構造体変数。
*/
void printStudentData(struct Student_tag student)
{
    printf( "name: %s\n", student.name );
    printf( "grade: %d\n", student.grade );
    printf( "class: %d\n", student.class );
    printf( "score: %d\n", student.score );
}

実行結果:

name: Saitou Takashi
grade: 2
class: 3
score: 80

構造体変数 student の初期化のところを見て下さい。「.name = "Saitou Takashi"」という記述によって、name というメンバのところに初期値 "Saitou Takashi" が与えられます。このように、「.メンバ名 = 初期値」という構文が使えるようになりました。

パディング

構造体のメンバは、メモリ上では、メンバの宣言順通りに並びます。しかし、メンバとメンバの間や、最後のメンバの後ろに、余分な空き領域が入ることがあります。このような領域を、パディング(詰め物)といいます。

パディングとメモリの使われ方に関する詳細な話はここではせず、第37章で改めて取り上げることにします。ここでは、現時点でもパディングの存在を感じることがある場面を紹介するに留めます。

構造体全体の大きさを sizeof演算子で調べてみると、メンバの大きさをすべて足し合わせたものよりも大きくなることがあります。これはまさに、メンバ間や末尾にパディングが入る(ことがある)ためです。

#include <stdio.h>

struct Data_tag {
    short s;
    char  c;
};

int main(void)
{
    printf( "sizeof(short) == %u\n", sizeof(short) );
    printf( "sizeof(char) == %u\n", sizeof(char) );
    printf( "sizeof(struct Data_tag) == %u\n", sizeof(struct Data_tag) );
    
    return 0;
}

実行結果:

sizeof(short) == 2
sizeof(char) == 1
sizeof(struct Data_tag) == 4

2つのメンバの大きさを足し合わせても 3バイトにしかなりませんが、構造体全体の大きさは 4バイトになっています。この差の 1バイトは、パディングによって生み出されているものです。

なお、パディングがどの位置にどのように入るのかは、処理系定義です。

また、パディングの部分は、何らかの方法で明示的に値を入れない限り、不定値が入った状態です。これはたとえ、構造体変数が静的記憶域期間を持っていて、暗黙的に初期化されたとしてもです。暗黙的な初期化の対象は、「各メンバ」であって、パディングは含まれていません。


メンバ全体をまとめた操作

構造体変数を、そのまま引数や戻り値で受け渡しできるのは、同じ型の構造体変数はそのまま代入できるからです。 試してみましょう。

#include <stdio.h>

#define STUDENT_NAME_LEN 32         /* 生徒の名前データの最大長 */

/* 生徒のデータ */
struct Student_tag {
    char  name[STUDENT_NAME_LEN];   /* 名前 */
    int   grade;                    /* 学年 */
    int   class;                    /* 所属クラス */
    int   score;                    /* 得点 */
};


void printStudentData(struct Student_tag student);

int main(void)
{
    struct Student_tag student1 = { "Saitou Takashi", 2, 3, 80 };
    struct Student_tag student2;

    student2 = student1;

    printStudentData( student1 );
    printStudentData( student2 );

    return 0;
}

/*
    生徒のデータを出力する。
    引数:
        student: 出力するデータを集めた構造体変数。
*/
void printStudentData(struct Student_tag student)
{
    printf( "name: %s\n", student.name );
    printf( "grade: %d\n", student.grade );
    printf( "class: %d\n", student.class );
    printf( "score: %d\n", student.score );
}

実行結果:

name: Saitou Takashi
grade: 2
class: 3
score: 80
name: Saitou Takashi
grade: 2
class: 3
score: 80

ところで、ここに少し不思議な点があります。struct Student_tag という構造体のメンバには、char型の配列が含まれています。これまでに何度か見てみたように、文字列をそのまま代入することはできず、strcpy関数の力を借りる必要があったはずです。

実は、構造体同士の代入は、中身が何であれ成功します。 メンバとして配列が含まれていれば、その配列の要素をそのままコピーしてくれるので、問題なく動作します。

メンバとしてポインタ変数(第31章)が含まれている場合、ポインタ変数がコピーされます。そのため、同じメモリアドレスを指すポインタ変数が2つあるという状況になることに注意が必要です。


代入はまとめて行えますが、比較はできません。つまり、「if(student1 == student2)」のような比較は、(出来ても良さそうなものですが)出来ません。これはコンパイルエラーになります。

構造体の比較を行うときは、メンバを1つ1つ比較するしかありません。これは面倒な作業ですし、後からメンバが増えたときに、比較の対象を忘れずに追加しなければならないので注意が必要です。

memcmp関数という標準ライブラリ関数を使って比較しようとする人がいますが、それは恐らく正しくありません。memcmp関数を使うと、パディングの部分まで含めて比較してしまいますが、パディングの部分に何があるかは不定だからです。この話題は、第34章で扱っています。

タグが省略される場合

構造体型を定義する際、通常はタグ名を指定します。 しかし、タグ名を省略できる場合があります。 それは、次のように、構造体型の定義と、構造体変数の宣言を同時に行う場合です。

struct {
    型 メンバ名;
    型 メンバ名;
      :
} 変数名;

更に、初期値を与えることもできます。

struct {
    型 メンバ名;
    型 メンバ名;
      :
} 変数名 = { 1つ目のメンバの初期値, 2つ目のメンバの初期値, … };

この方法を使った場合、この章の最初のサンプルは次のように書き換えられます。

#include <stdio.h>
#include <string.h>

int main(void)
{
    #define STUDENT_NAME_LEN 32			/* 生徒の名前データの最大長 */

    /* 生徒のデータ */
    struct {
        char  name[STUDENT_NAME_LEN];   /* 名前 */
        int   grade;                    /* 学年 */
        int   class;                    /* 所属クラス */
        int   score;                    /* 得点 */
    } student;

    strcpy( student.name, "Saitou Takashi" );
    student.grade = 2;
    student.class = 3;
    student.score = 80;

    printf( "name: %s\n", student.name );
    printf( "grade: %d\n", student.grade );
    printf( "class: %d\n", student.class );
    printf( "score: %d\n", student.score );

    return 0;
}

実行結果:

name: Saitou Takashi
grade: 2
class: 3
score: 80

タグ名を省略してしまうと、当然ながらソースコード上の他の箇所ではタグ名が使えません。 そのため、関数にこの構造体を渡そうと思っても、タグ名が無いので、仮引数を記述できません。 printStudentData関数を諦めて main関数の中に展開しましたが、関数化(部品化)していた以前のコードの方が良いスタイルでしょう。

typedef

構造体型の定義でタグ名を省略してしまうと、型名を表現できなくなってしまいますが、回避策もあります。 そのためには、typedefキーワードを使います。

typedef を使うと、既存の型名に別名を付けられます。typedef は以下の構文で使用します。

typedef 既存の型名 新しい型名;
既存の型名 typedef 新しい型名;

ほとんどの場合、上の方の書き方が使われます。 「既存の型名」というのは、例えば、int や double といったものですが、自分で定義した構造体型や、配列型を指定することもできます。 「新しい型名」は自由に付けられます。

1つの利用法は、環境によって大きさが異なってしまう型への対応です。 例えば、int型が 32ビットの環境ばかりとは限りません。 そこで、必ず 32ビットであることが保証された型を、typedef を利用して作り出します。

typedef int int32;   /* int型が 32ビットの環境ならこちら */
typedef long int32;  /* long型が 32ビットの環境ならこちら */

環境に合わせて、どちらか一方だけが有効になるようにしておき(#if とか #ifdef とかで切り分けられるかもしれません)、あとはプログラム中で、32ビットの整数型が必要なときには、int32 という型名を使うようにすればよいです。現実には、そんなに簡単にはいかないこともありますが、こういう手法自体は有効です。

また、C99 には <stdint.h> という新しい標準ヘッダに、同様の趣旨の定義があります。

新しい型名は、所詮、新しい「名前」に過ぎず、新しい「型」が出来た訳ではありません。 もし、まったく別の型が作られたのであれば、上記の例で、int32型の変数を int型の変数には代入できないことになりますが、実際には代入できます。 両者は、同じ型の別の名前なのです。

#include <stdio.h>

int main(void)
{
    typedef int int32;

    int   a = 99;
    int32 b = a;
    int   c = b;

    printf( "%d\n", a );
    printf( "%d\n", b );
    printf( "%d\n", c );

    return 0;
}

実行結果:

99
99
99


構造体の話に戻りましょう。 まずは、最初のサンプルプログラムを typedef を使ったものに書き換えてみます。

#include <stdio.h>
#include <string.h>

#define STUDENT_NAME_LEN 32         /* 生徒の名前データの最大長 */

/* 生徒のデータ */
struct Student_tag {
    char  name[STUDENT_NAME_LEN];
    int   grade;
    int   class;
    int   score;
};
typedef struct Student_tag Student;



void printStudentData(Student student);

int main(void)
{
    Student student;

    strcpy( student.name, "Saitou Takashi" );
    student.grade = 2;
    student.class = 3;
    student.score = 80;

    printStudentData( student );

    return 0;
}

/*
    生徒のデータを出力する。
    引数:
        student: 出力するデータを集めた構造体変数。
*/
void printStudentData(Student student)
{
    printf( "name: %s\n", student.name );
    printf( "grade: %d\n", student.grade );
    printf( "class: %d\n", student.class );
    printf( "score: %d\n", student.score );
}

実行結果:

name: Saitou Takashi
grade: 2
class: 3
score: 80

普通に、構造体型の定義を行い、Student_tag というタグ名を付けました。 その直後に typedef を使って、struct Student_tag の別名「Student」を定義しています。 以降、「struct Student_tag」と書いていた箇所はすべて「Student」に変更していますが、正しく動作しています。

さて本題は、タグ名を省略しつつも、型名が使えるようにしたいという話でした。 そのためには、構造体型の定義と同時に、typedef を使います。

typedef struct {
    char  name[STUDENT_NAME_LEN];   /* 名前 */
    int   grade;                    /* 学年 */
    int   class;                    /* 所属クラス */
    int   score;                    /* 得点 */
} Student;

こう書くと、タグ名の無い構造体型が定義され、その無名の構造体型に対して「Student」という別名を定義できます。この構文は、以下のようになっています。

typedef struct タグ名(省略可) {
    型 メンバ名;
    型 メンバ名;
      :
} 型名;

「タグ名」は省略できますが、書いても構いません。その場合は、タグ名でも別名でも、同じ型名として使用できます。タグ名と別名を両方とも使う場合、タグ名の方に「_tag」を付ける命名規則は効果的です。しかし実のところ、タグ名と別名がまったく同じ名前であっても、文法上は許されます

ところで、タグ名を省略して、typedef で別名を付ける価値がどの程度あるのか疑問でしょう。基本的には、型名を使うたびに「struct」を記述する手間が減り、ソースコード上の見た目の文字数を減らす効果があるだけです。とはいえプログラミングをするにあたっては、手間が減り、見た目もすっきりするのなら、それなりに価値があることです。

C++ では、構造体型を使うときに structキーワードを省略できるため(Modern C++編【言語解説】第2章)、typedef を使った場合の方が、C++ の書き方に近いという見方もあります。

自己参照

構造体のメンバとして、構造体変数を持たせることも可能ですが、自分自身と同じ型は不可です。

struct Student_tag {
    int num;
    struct Student_tag other_student;  /* コンパイルエラー */
};

これは実現したい場面もあるので、ポインタを使った解決策があります。これについては、第37章で取り上げます。

構造体のネスト

構造体型をネストさせて、次のように記述することが可能です。

struct Student_tag {
    struct Score_tag {
        int  math;
        int  english;
    };

    char  name[STUDENT_NAME_LEN];
};

内側にある構造体のメンバへのアクセスも、ドット演算子を使ってアクセスできます。 ネストしていても、アクセスの仕方は変わりません。

struct Student_tag student;
student.math = 75;
student.english = 85;

また、内側の構造体型の変数を宣言できます。

struct Score_tag score;
score.math = 75;
score.english = 85;

これではもはや Student_tag の存在が見えず、ネストさせていることに意味がありません。そもそも、構造体をネストさせることの価値は、あまり無いと思います。

C++ では事情が異なり、内側の構造体を使うには、外側の構造体型の名前で修飾する必要があります(C++編【言語解説】第24章)。C++ では、構造体をネストさせることに、それなりの価値があります。


練習問題

問題① 平面上にある点の座標(x,y) を表現できる Point構造体を作成して下さい。 座標は int型で表現するものとします。

問題② 問題①の Point構造体について、次のような関数を作成して下さい。

問題③ 問題①の Point構造体を使って、四角形を表現できる Rect構造体を作成して下さい。


解答ページはこちら

参考リンク



更新履歴

'2018/6/1 「パディング」の項の内容を調整。特にメモリアドレスに関する知識が必要な話題をできるだけ避けて、第37章で扱うようにした。

'2018/5/18 「構造体変数の初期化」の項を修正。
-- 明示的に初期化しなかったときの結果について追記。
-- 初期化子が不足していたり、多かったりしたときの動作について追記。
-- 別の構造体変数を使った初期化について追記。

'2018/5/14 「構造体変数の初期化」の項に、配列の場合について追記。

'2018/3/1 全面的に文章を見直し、修正を行った。

'2018/2/26 「指示付きの初期化」という表現を、「要素指示子」に置き換えた。

≪更に古い更新履歴を展開する≫



前の章へ(第25章 配列と文字列)

次の章へ(第27章 いろいろな式)

C言語編のトップページへ

Programming Place Plus のトップページへ


はてなブックマーク Pocket に保存 Twitter でツイート Twitter をフォロー
Facebook でシェア Google+ で共有 LINE で送る rss1.0 取得ボタン RSS
管理者情報 プライバシーポリシー