構造体 | Programming Place Plus C言語編 第26章

トップページC言語編

このページの概要

以下は目次です。


構造体

構造体 (structure) は、1個以上の変数をひとかたまりにまとめた型です。構造体に含まれる1つ1つの変数は、メンバ (member) や要素などと呼ばれます。

構造体は int型や double型などと同様に型の種類であり、構造体型 (structure type) と呼びます。int などの基本的な型と大きく違うのは、型の詳細を決めるのがプログラマーの役目であるという点です。つまり、構造体を使うにはまず、構造体型を定義し、そのあと、その型の変数を定義するという流れになります

単に「構造体」といったときには、構造体型のことを指していることも、構造体型の変数のことを指していることも、あるいは両方をなんとなくひとまとめにしたように言っていることもあります。

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

配列と構造体の代表的な違いは以下の点です。

  1. 配列の要素はすべて同じ型であるが、構造体の要素はそれぞれ異なってもいい
  2. 配列の要素は添字を使ってアクセスできるが、構造体には添字はない
  3. 配列の要素は隙間なく連続的に並んでいるが、構造体の要素はその保証がない

1番については「構造体型を定義する」で、2番については「メンバにアクセスする」で、3番については「パディング」でそれぞれ取り上げます。

構造体型を定義する

構造体型は次の構文で定義します。

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

struct (struct keyword) は、構造体を意味するキーワードです。

「タグ名」には、タグ(構造体タグ) (tag、structure tag) に付ける名前を記述します。タグとは、複数の構造体型を区別するために使う名前です。定義する構造体型が何を表現しているものなのかが分かるように名前を決めます。タグは使うことがなければ省略できます(この話題はこのあと2箇所 [1][2] で取り上げます)。

「型 メンバ名」は、構造体型に含まれる各メンバの宣言です。これはいつもの変数宣言と同じで、型名と名前を書けばいいですが、初期値を指定することはできません。また、static や extern を付加することもできません。なお、この構造体型に所属するメンバの名前なので、ほかの場所で宣言されている名前とは区別が付けられるため、ほかの名前と被っても構いません。

このように1つ1つのメンバの宣言時に型名を与えるので、それぞれ異なる型にできます。

【C++プログラマー】C言語の構造体は変数をまとめるだけのものであり、メンバ関数を作ることはできませんし、アクセス指定や継承などの機能もありません。

例として、生徒の情報をまとめる構造体型を次のように定義できます。

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

この定義は、関数定義の外側にも、どこかのブロックの内側にでも記述できます。構造体型の変数を定義するときには、その位置から構造体型の定義がみえていなければなりません。ブロック内で定義した場合は、そのブロックの外側からは使用できません。

複数のソースファイルから使いたい場合は、ヘッダファイルに記述し、#include で取り込みます。

構造体変数を宣言する

構造体型の定義ができたら、構造体変数を宣言できます。変数宣言の方法に違いはありません。たとえば、Student構造体の構造体変数を次のように宣言できます。

struct Student student;

構造体型の名前は、structキーワードと構造体タグの組み合わせによって表現されます。

いちいち structキーワードを使わずに済ませるため、別名を与える方法があります。あとで取り上げます

【C++プログラマー】C++ では structキーワードは不要ですが、C言語では必要です。

いつものように、変数student が自動記憶域期間を持つのなら、各メンバは初期化されていません。静的記憶域期間を持つのなら、0 に相当する値で初期化されます。

【C++プログラマー】各メンバの宣言時に初期値を与える構文もないですし、コンストラクタもないので、初期化を強制する方法はありません。

メンバに初期値を与えるには、構造体変数を定義するときに、まとめて初期化子を与えます。

struct タグ名 変数名 = {初期化子並び};

{} の内側に、1つ以上の初期化子を , で区切って並べます。1つ目の初期化子は1つ目のメンバに、2つ目の初期化子は2つ目のメンバに適用されます。

配列の場合と同様、初期化子の個数が、実際のメンバの個数よりも少ない場合は、残りのメンバにはデフォルトの初期値が与えられます。デフォルトの初期値は、静的記憶域期間の変数に与えられるデフォルトの初期値の規則と同様で、大ざっぱにいえば 0 です(第22章)。初期化子の方が多い場合は、コンパイルエラーになります。

可能であれば、この初期化方法を使うのが(メンバが初期化されていない瞬間がなくなるので)安全ですが、あとからメンバを増やしたり減らしたりすると、初期値を与える相手がいつのまにか変わってしまう可能性があることに注意が必要です。この問題への対処として、要素指示子 (designated initializer) というものがあります。これは、配列のときに紹介したものと同様に、特定の要素を選択的に初期化できる機能です。あとで、あらためて取り上げます

Student構造体を使ったプログラム例は次のようになります。

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

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

    // ...
}

すでに存在する同じ構造体型の構造体変数のコピーを作ることもできます。

struct Student student2 = student1;  // student1 と同じ内容の student2 を定義

代入も同じ構造体型であれば可能です。

struct Student student1;
struct Student student2;

student2 = student1;

構造体型の定義と構造体変数の宣言をまとめる

構造体型の定義と、その型の変数の宣言をまとめて書くことができます。初期化子の有無は自由です。

struct Student_tag {
    char  name[32];                 // 名前
    int   grade;                    // 学年
    int   class;                    // 所属クラス
    int   score;                    // 得点
} student = {"Saitou Takashi", 2, 3, 80};

このコードで構造体変数を1つ定義できているわけですから、この続きのコードの中にはもう struct Student_tag という記述が登場しないかもしれません。もしそうなら、構造体タグ名は不要なものだといえるため、省略してしまうことができます。

struct {
    char  name[32];                 // 名前
    int   grade;                    // 学年
    int   class;                    // 所属クラス
    int   score;                    // 得点
} student = {"Saitou Takashi", 2, 3, 80};

もしこの続きのコードの中で、構造体型の名前を記述しなければならない箇所があるのなら(たとえば、関数の仮引数や戻り値の型として使うなど)、タグ名を省略することはできません。

メンバにアクセスする

構造体変数を宣言できるようになったので、メンバにアクセスしてみます。メンバは次の構文でアクセスできます。

構造体変数名.メンバ名

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

実際のプログラムは次のようになります。

#include <stdio.h>

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

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

int main(void)
{
    struct Student_tag student = {"Saitou Takashi", 2, 3, 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);
}

実行結果:

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

いつものように、できるだけ変数が未初期化な瞬間が少なくなることが望ましいといえるので、可能なら、変数定義と同時にすべてのメンバを初期化するといいでしょう。正しい値で初期化できないとしても、いったん 0 で初期化しておくのは1つの手です。{} の内側には最低1つの初期化子が必要ですが、あとは省略してしまえば、自動的に 0 が入るので、次のように書けば、すべてのメンバを 0 に相当する値で初期化できます。

struct Student_tag student = {0};

0 で初期化することが正しいといっているのではないことに注意してください。たとえば、grade(学年)が 0 なのは、おそらくむしろ不正なデータです。それでも、いつも同じ「正しくない状態」になるので、どう「正しくない」のかすら定まらない未初期化な状態よりは良いといえます。

【C++プログラマー】C言語では {} の内側を空にはできません。

構造体型の配列

もちろん、構造体型の配列を定義することも可能です。

#include <stdio.h>

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

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

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

    const size_t size = sizeof(students) / sizeof(students[0]);
    for (size_t i = 0; i < size; ++i) {
        printf("name: %s\n", students[i].name);
        printf("grade: %d\n", students[i].grade);
        printf("class: %d\n", students[i].class);
        printf("score: %d\n", students[i].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

配列の1つ1つの要素の型が、struct Student_tag ということになります。配列の初期化のための {} の内側には、各々の struct Student_tag を初期化するための {} が入っています。

配列内の構造体のメンバにアクセスするときには、students[i].name のように、まず配列内の1要素を添字演算子によって特定してから、メンバアクセス演算子を使ってメンバにアクセスするという流れになります。

typedef

構造体型の名前を毎回「struct Student_tag」のように記述することは少々面倒ではあります。そこで、第19章で取り上げた typedef を活用できます。

#include <stdio.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;

int main(void)
{
    Student student = {"Saitou Takashi", 2, 3, 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);
}

実行結果:

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

Student_tag という構造体タグ名の構造体を定義してから、typedef によって 別名 Student を定義しています。以降、struct Student_tagStudent は同じものを意味していることになります。

構造体型の定義と、別名の定義は同時に行えます。

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

また、タグ名は使わないなら省略できるのでした。別名さえあれば、タグ名は不要ですから省略してしまえます。

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

比較

構造体変数どうしの比較は直接的には行えません。たとえば、student1 == student2 のように比較しようとしてもコンパイルエラーになります。構造体変数どうしで比較したい場合は、メンバ1つ1つを別個に比較する必要があります。

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

【上級】このあと「パディング」のところでも触れますが、memcmp関数を使って比較することは間違っているので注意してください。memcmp関数はパディングを含めて比較するため、パディング部分が不定の状況では正しい比較にならないからです。

#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;                    // 得点
};

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

    if (strcmp(student1.name, student2.name) == 0
     && student1.grade == student2.grade
     && student1.class == student2.class
     && student1.score == student2.score
    ) {
        puts("Equal.");
    }
    else {
        puts("Not equal.");
    }
}

実行結果:

Equal.

すべてのメンバを比較するために、論理AND演算子(第15章)を使って条件式をつなげ合わせます。あとから構造体型にメンバが追加される場合、条件式を増やしわすれないように注意が必要です。

比較する機会が何度もあるのなら、関数を作っておくのも手でしょう。

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

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

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

static bool is_equal_student(struct Student_tag s1, struct Student_tag s2)
{
    return strcmp(s1.name, s2.name) == 0
        && s1.grade == s2.grade
        && s1.class == s2.class
        && s1.score == s2.score
        ;
}

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

    if (is_equal_student(student1, student2)) {
        puts("Equal.");
    }
    else {
        puts("Not equal.");
    }
}

実行結果:

Equal.

ここで、関数に構造体を2つ渡しているわけですが、引数を渡すという行為は、データのコピーを作っているということなので、構造体のような大きなデータは処理速度の低下につながることに注意が必要です。

【上級】この速度への影響は、第33章で取り上げる、ポインタという機能を使うことで軽減できます。また、関数を呼び出すこと自体の処理速度すらも気にする場合、インライン関数(第57章)によって解決できるかもしれません。

要素指示子

配列のページ(第25章)で解説した要素指示子を構造体でも使用できます。

【C89/95 経験者】この機能は C99 で追加されたものです。

【C++ プログラマー】仕様に違いはありますが、C++20 でも同じ方法が使えるようになりました。1

次のサンプルプログラムは、要素指示子の使用例です。

#include <stdio.h>

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

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

int main(void)
{
    struct Student_tag student = {
        .name = "Saitou Takashi",
        .grade = 2,
        .class = 3,
        .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);
}

実行結果:

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

「.メンバ名 = 初期値」という構文で、任意のメンバを初期化できます。

初期値が明示されていないメンバがある場合、静的記憶域期間の場合に与えられるデフォルトの初期値の規則と同様に、0 に相当する値で初期化されます。2

複合リテラル

複合リテラル (compound literal) を使うことによって、構造体型のリテラルを記述できます。

【C89/95 経験者】この機能は C99 で追加されたものです。

複合リテラルの構文は以下のとおりです。

(型名){初期化子並び}

「型名」のところに struct Student_tag のような構造体型の名前を入れ、「初期化子並び」のところに、各メンバの初期値を順番に , で区切って並べます。初期化に関するルールは、「構造体変数を宣言する」のところで説明したとおりです。また、要素指示子を使っても構いません。

「初期化子並び」で与えられた初期値をもった、名前のないオブジェクト(つまり名前のない構造体変数のようなもの)が作られます。名前がないので、作られたオブジェクトを自由に使うことはできず、それゆえ書き換えることもできないので、定数(リテラル)のように扱えるということになります。

複合リテラルによって作られたオブジェクトは構造体変数に代入したり、関数の実引数にしたりといったかたちで利用します。

#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;                    // 得点
};

static void print_student_data(struct Student_tag student);

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

    print_student_data((struct Student_tag){"Takada Shinji", 3, 3, 65});
}

// 生徒のデータを出力する。
// student: 出力するデータを集めた構造体変数
static void print_student_data(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: Takada Shinji
grade: 3
class: 3
score: 65

【C++プログラマー】C++ では集成体に対して初期化子リストを渡せるので、student = {"Saitou Takashi", 2, 3, 80}; のように、もっとシンプルに書けます。

パディング

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

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

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

#include <stdio.h>

struct Data_tag {
    short s;
    char  c;
};

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

実行結果:

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

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

パディングがどの位置にどのように入るのかは、処理系定義です。そのため、この実行結果と同じにならない処理系もありえます。

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

【上級】したがって、パディングの部分が不定のまま、memcmp関数を使って構造体を比較することは間違っています。

【上級】パディングに値を入れる「なんらかの方法」には、memset関数(第34章)などがあります。

ほかの構造体変数を使った初期化や代入は、そのオブジェクトのコピーを取っているわけですから、パディングの部分についてもコピーされることになります。

自己参照

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

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

このような構造は不可能ですが、似たような状態を作ることはできます。第37章で取り上げます。

構造体の入れ子

構造体型の定義を入れ子にして、次のように記述することが可能です。

#include <stdio.h>

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

struct Student_tag {
    char              name[STUDENT_NAME_LEN];
    int               grade;
    int               class;

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

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

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

実行結果:

name: Saitou Takashi
grade: 2
class: 3
math score: 72
english score: 77

構造体型の定義の中に構造体型の定義があることは許されますが、微妙に意図と違うかもしれません。サンプルプログラムのように、student.math とか student.english のように、内側の構造体型のメンバであることは特に意識することなく、アクセスできています。

また、内側にある構造体型の変数を定義することができます。

#include <stdio.h>

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

struct Student_tag {
    char              name[STUDENT_NAME_LEN];
    int               grade;
    int               class;

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

int main(void)
{
    struct Score_tag score = {85, 40};

    printf("math score: %d\n", score.math);
    printf("english score: %d\n", score.english);
}

実行結果:

math score: 85
english score: 40

これも、内側にある構造体型だということは特に意識することがありません。

【C++プログラマー】これらの挙動は C++ とは異なります。1つ目の例は C++ では Score_tag型のメンバを宣言しなければなりません。2つ目の例では Student_tag::Score_tag のように、スコープ解決演算子による修飾が必要です。


練習問題

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

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

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


解答ページはこちら

参考リンク


更新履歴

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



前の章へ (第25章 配列)

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

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

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る