【C言語】構造体について初心者向けに分かりやすく解説

構造体解説ページのアイキャッチ

このページにはプロモーションが含まれています

このページではC言語における「構造体」について解説していきます。

構造体を利用することで下記のようなメリットがあります。

  • ソースコードを読むのが楽
  • ソースコードを書くのが楽
  • プログラムを拡張するのが楽

これらのメリットの詳細についてはこのページの後半で解説しています。

構造体をあまり使ったことがない方も、是非この機会に構造体への理解を深め、構造体を積極的に使ってみてください!

構造体とは

構造体とは「関連する複数のデータ1つにまとめて管理する型」のことです。

構造体の概念図

もともとC言語には charint などの基本的な型が用意されています。構造体もその型の1つとして考えて良いです。

ただし、charint などとは異なり、構造体は作成するプログラムに合わせて自分で好きなように作ることができます。どのデータを1つにまとめるのかも自分で決めることができます。

例えば下図は「生徒」を表す構造体の例を図示したものになります。

これは、生徒に関連する複数のデータ(年齢・学生番号・名前)を1つにまとめて管理する型です。

生徒を表す構造体の概念図

構造体を構成する各データは「メンバ」と呼ばれます。

例えば上の例では、構造体は下記の3つのメンバを持つことになります

  • 年齢
  • 学生番号
  • 名前

構造体は型ですので、変数宣言を行うことで、その型の変数をプログラムで使用することができます。その変数それぞれが構造体を構成するメンバを持つことになります。

変数を宣言するだけだと、ただの構造体の形をした変数ですが、

変数宣言後の変数の状態

各変数のメンバに具体的な値を設定することで、それぞれの変数が異なる実体を表すようになります。

各生徒の情報をメンバに設定した状態

こんな感じで構造体を枠組み・各変数をオブジェクトとして捉えてプログラミングすることもできます。オブジェクト指向のクラスに似ていると考えて良いです。

これにより、C言語でもオブジェクトを扱う感じで少しは直感的に分かりやすいプログラミングができるようになると思います。

この構造体の他の型(charint など)との一番の違いは「メンバを持つ」ことです。

メンバを扱うときに他の型とは異なる記述をする必要がありますが、それ以外は基本的な考え方は他の型と同じです。

なので構造体と聞いて「難しそう」とか身構える必要はないです!

構造体の基本的な使い方

それでは早速「構造体」の使い方を見ていきましょう!

スポンサーリンク

構造体を宣言する

構造体は前述の通り、自分で作成することのできる型になります。

自分で好きなように作成することができますが、変数宣言等で使用する前に、どのような型であるかを宣言しておく必要があります。

この構造体の宣言を行うことで、その構造体の枠組みをしたデータが扱えるようになります。

生徒を表す構造体の概念図

宣言は下記のような形で行います。最後の ; を付け忘れるとコンパイルエラーになるので注意しましょう。

構造体の宣言
struct 構造体タグ名 {
    型名 メンバ名;
    型名 メンバ名;
    型名 メンバ名;
    ・・・・
};

これにより、宣言を行ったソースコードにおいて struct 構造体タグ名 という名前の新たな型が使用できるようになります。

型名 メンバ名; の部分は基本的な型である charint 型などの変数を宣言するときと同様の書き方になります。

例えば下記は前述した「生徒を表す構造体」の宣言の具体的な例になります。

構造体の宣言の例1
struct student {
    char age; /* 年齢 */
    int number; /* 学生番号 */
    char name[100]; /* 名前 */
};

メンバの型は、C言語で使用できるものであればなんでもオッケーです。

ポインタ型でも良いですし、配列でも良いです。構造体の型を指定することも可能です。

構造体の宣言の例2
struct student {
    char age; /* 年齢 */
    int number; /* 学生番号 */
    char name[100]; /* 名前 */
    struct student *lover; /* 好きな生徒 */
};

ただし、自身の構造体の型のメンバを持たせる場合は、必ずポインタにする必要があります。

構造体の変数を宣言する

構造体を宣言することで、その構造体の型を使用することができるようになります。

ただし、構造体の型を宣言するだけでは意味がなく、その構造体の型の変数を宣言することで、その型のデータをプログラム内で使用できるようになります。

charint も、その型のデータを扱うためには変数宣言が必ず必要ですよね?それと一緒です。

構造体の変数宣言を行うことで、構造体の枠組みの形をした実体(オブジェクト)が作成されます。

変数宣言後の変数の状態

構造体の変数宣言を行うためには、他の型同様に、下記のような形で記述すれば良いです。

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

例えば構造体タグ名が student、変数名が x の場合、下記のようにして変数宣言を行います。

構造体の変数宣言の例1
struct student x;

 これも他の型同様、複数の変数を一度に宣言することもできます。

構造体の変数宣言の例2
struct student x, y;

構造体の型名は、構造体タグ名だけでなく、struct も付ける必要がある点に注意が必要です。

struct を付け忘れるとコンパイルエラーになります。

ただし毎回 struct を付けるのが面倒な場合は、typedef を利用すれば struct を省略した形で構造体の型名を使用することができるようになります。

これに関しては構造体名の再定義で解説しています。

構造体の変数を使用する

構造体の変数を宣言すれば、その変数を使用することができるようになります。

基本的に、構造体の変数を使用する際には、構造体のメンバに対してアクセスし、そのメンバに値を格納したり、メンバから値を取得したりします。

メンバに対して値を格納することで、各変数を異なる実体として区別して扱うことができるようになります。例えば生徒の構造体の変数のメンバに年齢・学生番号・名前を格納してやれば、それぞれを個別の生徒として区別して扱うことができるようになります。

各生徒の情報をメンバに設定した状態

構造体のメンバにアクセスする場合には、変数名とメンバ名の間に . (ピリオド)を付けます。

メンバへのアクセス
変数名.メンバ名

例えば構造体の変数名が x、メンバ名が number の場合、下記のように記述すれば、x のメンバ number10 が格納されることになります。

メンバへの値の格納
x.number = 10;

また下記のように記述すれば、x のメンバ number の値を取得することができます。

メンバからの値の取得
a = x.number;

メンバにアクセスするという点は、他の基本的な型である charint などとは異なりますが、メンバにアクセスした後は他の基本的な型と同じ使い方をすることができます

メンバそれぞれに型がありますので、その型に合わせた使い方をすれば良いだけです。

例えば構造体を宣言するで紹介した struct student には char name[100]; というメンバを持たせていますが、この name に文字列を格納したい場合は下記のように strcpy などを利用します(構造体の変数名を x としています)。

メンバへの値の格納2
strcpy(x.name, "hanako");

構造体であるかどうかは関係なく、上記は char 型の配列に文字列を格納するときと同じような処理ですよね?

こんな感じで、メンバにアクセスする必要はありますが、それ以外はそのメンバの型に合わせて、通常の型と同様にして使用してやれば良いだけです。

スポンサーリンク

構造体の配列

他の型同様に、構造体も配列として使用することができます。

下記のように変数宣言すれば、構造体を宣言するで紹介した struct student 型の配列を作成することができます。

構造体の配列
struct student array[100];

上記により array[0]array[1]、…、array[99] までの要素を持つ配列が作成されます。

このとき、各要素のメンバにアクセスする場合には下記のように配列の添字にインデックス(要素番号)を指定した上で、. (ピリオド)を付けてメンバを指定します。

構造体の配列のメンバへのアクセス
for (i = 0; i < 100; i++) {
    student[i].number = i;
}

構造体のポインタ

構造体は他の型同様に変数名の前に & を付けることで、その構造体の変数がメモリ上のどの位置に存在するかを示すアドレスを取得することができます。

さらに、これも他の型同様に、そのアドレスをポインタに指させることができます。

変数宣言やポインタに指させる方法も他の型と同じです。同じ型のポインタ変数を宣言し、そのポインタにアドレスを指させます。

構造体のポインタ
struct student x;
struct student *p;
p = &x;

これにより、px の先頭アドレスを指すことになります。

さらに、これも他の型同様、ポインタ変数の名前の前に * を付けることで、そのポインタが指している構造体の実体にアクセスすることができます(変数宣言時以外)。

ただし、あくまでも p が指すのは x の先頭アドレスですので、各メンバにアクセスするためには . を付ける必要があります。

ポインタを介した構造体のメンバへのアクセス
struct student x;
struct student *p;
p = &x;
(*p).number = 1000;
strcpy((*p).name, "taro");

* や . などたくさんの記号をつける必要がある&読みにくいので、代わりにアロー演算子を用いることも可能です。

アロー演算子を用いた構造体のメンバへのアクセス
struct student x;
struct student *p;
p = &x;
p->number = 1000;
strcpy(p->name, "taro");

構造体へのポインタからメンバにアクセスする場合(つまりメンバにアクセスする変数がポインタの場合)、上記のようにアロー演算子を用いて簡潔に記述することが可能です。

ただしポインタではなく構造体そのものからメンバにアクセスする場合は前述の通り . を使ってアクセスします。

アロー演算子を用いた構造体のメンバへのアクセス
struct student x;
struct student *p;
p = &x;
p->number = 1000; /* ポインタからアクセス */
x.number = 1000; /* 構造体の実体からアクセス */

このアロー演算子については下記ページで詳しく解説していますので、是非こちらのページも読んでみてください。

C言語のアロー演算子(->)を分かりやすく、そして深く解説

構造体の初期化

他の型同様に、構造体も変数宣言時に変数を初期化することができます。

下記のように、変数名の後に = {}; を付ければ構造体の初期化(構造体の各メンバの初期化)を行うことができます。

構造体の初期化1
struct student {
    char age; /* 年齢 */
    int number; /* 学生番号 */
    char name[100]; /* 名前 */
};

int main(void) {
    struct student x = {
        15, 124, "TARO"
    };
    /* 〜略〜 */
}

{} 内で各メンバに初期化時に格納したい値を指定します。メンバの並び順に合わせて、各メンバに格納したい値を指定します。

メンバ全てに 0 を設定したい場合は下記のように {} の中をにしてやれば良いです。

構造体の初期化2
struct student x = {};

スポンサーリンク

構造体のコピー

複数の構造体の変数を扱う場合、構造体のコピー(構造体の各メンバのコピー)を行いたくなる場合があります。

この構造体のコピーを行う方法は下記の3つがあります。

  • 各メンバを個別にコピー
  • 構造体全体を memcpy をコピー
  • 構造体全体を = でコピー

各メンバを個別にコピー

この方法は各メンバから値を取得し、他の変数のメンバにその値を格納することを繰り返すものになります。

各メンバに対してこの処理を記述する必要があるので、プログラムが長くなるというデメリットがあります。

各メンバを個別にコピー
struct student {
    char age; /* 年齢 */
    int number; /* 学生番号 */
    char name[100]; /* 名前 */
};

int main(void) {
    struct student x = {
        15, 124, "TARO"
    };
    struct student y;

    /* 構造体のコピー */
    y.age = x.age;
    y.number = x.number;
    strcpy(y.name, x.name);

    /* 〜略〜 */
}

構造体全体を memcpy をコピー

次に紹介するのは memcpy を使う方法です。

memcpy では第1引数で指定したアドレスに、第2引数で指定したアドレスのデータを第3引数で指定したサイズ分コピーする標準関数です。

ですので、下記のように memcpy に引数を指定して実行することで、構造体全体をコピーすることができます。

  • コピー先の変数のアドレス
  • コピー元の変数のアドレス
  • 構造体のサイズ

実際にコピーする例は下記のようになります。

各メンバを個別にコピー
struct student {
    char age; /* 年齢 */
    int number; /* 学生番号 */
    char name[100]; /* 名前 */
};

int main(void) {
    struct student x = {
        15, 124, "TARO"
    };
    struct student y;

    /* 構造体のコピー */
    memcpy(&y, &x, sizeof(struct student));

    /* 〜略〜 */
}

スポンサーリンク

構造体全体を = でコピー

構造体は下記のように構造体の変数に = で他の構造体の変数を代入することで、構造体のコピーを行うことができます。

基本的にC言語の他の型では、= で格納できるのは1つの値のみです。

ですが、構造体の場合は構造体全体をコピーし、全てのメンバに一度に値を格納することができます。ここはちょっと他の型とは異なる点ですね。

各メンバを個別にコピー
struct student {
    char age; /* 年齢 */
    int number; /* 学生番号 */
    char name[100]; /* 名前 */
};

int main(void) {
    struct student x = {
        15, 124, "TARO"
    };
    struct student y;

    /* 構造体のコピー */
    y = x;

    /* 〜略〜 */
}

ちなみに私が調べた感じだと、= でコピーした場合も、memcpy でコピーした場合も、内部での処理は同じになるようでした(なのでどちらを使っても良い)。

関数での構造体の利用

これも他の型同様、構造体を関数の引数にしたり、関数の戻り値にすることも可能です。

構造体を関数の引数にする

構造体を関数の引数にする場合、構造体の型名を引数の型にしてしてやれば良いだけです。

構造体を引数にする例
void printStudentInfo(struct student a) {
    printf("age = %d\n", a.age);
    printf("number = %d\n", a.number);
    printf("name = %s\n", a.name);
}

ただし構造体のサイズでも解説しますが、構造体のサイズは各メンバのサイズの合計(あるいは合計以上)になるので、他の型(charint など)に比べてサイズが大きくなりがちです。

また引数で渡されるデータは、関数実行時にコピーされ、そのコピーされたデータが関数に渡されることになります。重要なのはコピーが発生するという点で、コピーは引数で渡すデータが大きいほど時間がかかります。

つまり、構造体の型をそのまま引数に指定すると、関数の処理時間が長くなってしまう可能性があるので、引数の型としては構造体そのものではなく、構造体のポインタ型にする方が良いです。

これにより、関数実行時にコピーされるサイズはポインタのサイズ(8バイト or 4バイト)で済み、処理時間がなくなる可能性も排除することができます。

上記関数の引数をポインタ型に変更したものは下記のようになります(プログラム全体を載せています)。

構造体のポインタを引数にする例
#include <stdio.h>

struct student {
    char age; /* 年齢 */
    int number; /* 学生番号 */
    char name[100]; /* 名前 */
};

void printStudentInfo(struct student *);

void printStudentInfo(struct student *a) {
    printf("age = %d\n", a->age);
    printf("number = %d\n", a->number);
    printf("name = %s\n", a->name);
}

int main(void) {
    struct student x = {
        18, 1021, "TARO"
    };

    printStudentInfo(&x);
    return 0;
}

関数の引数を構造体のポインタ型にした場合、関数内でメンバにアクセスする際には「アロー演算子」を利用する必要がある点に注意してください。

スポンサーリンク

構造体を関数の戻り値にする

構造体を関数の戻り値にすることも可能です。

構造体を戻り値にする例
#include <stdio.h>
#include <string.h>

struct student {
    char age; /* 年齢 */
    int number; /* 学生番号 */
    char name[100]; /* 名前 */
};

struct student newStudent(char, int, char*);

struct student newStudent(char age, int number, char *name) {
    struct student y;

    y.age = age;
    y.number = number;
    strcpy(y.name, name);

    return y;
}

int main(void) {
    struct student x;

    x = newStudent(18, 1021, "TARO");

    printf("age = %d\n", x.age);
    printf("number = %d\n", x.number);
    printf("name = %s\n", x.name);
    
    return 0;
}

構造体のサイズ

次に構造体のサイズについて解説しておきます。

特にアドレスやポインタについて自信がない方には難しい内容かもしれませんが、そういった方は下記だけでも覚えておくと良いと思います。

  • 構造体のサイズの取得は sizeof 関数で行える
  • 構造体のサイズの基本的な考えk他は各メンバのサイズの足し算
  • ただしアライメントにより足し算結果よりもサイズが増えることがある
  • 構造体のメンバの順番を変更することでサイズを削減することができる可能性がある

それでは詳しい内容を説明していきます。

構造体のサイズの取得

C言語では、それぞれの型にはサイズがあります。

変数宣言を行った際には、その変数用にその変数の型分のサイズがメモリから確保され、そのメモリを利用して変数に値を格納したり、そのメモリから変数の値を取得したりすることができます。

変数がメモリ上に配置される様子

例えば char のサイズは1バイト、int のサイズは4バイトというのが一般的です(環境によっては異なる可能性もあります)。

型のサイズは sizeof 関数を利用して取得することができます。

型のサイズの取得
printf("%u\n", sizeof(char));

スポンサーリンク

基本的な構造体のサイズの計算方法

構造体も同様にサイズがあり、構造体のサイズの基本的な考え方は各メンバのサイズの足し算です。

例えば下記の構造体に関して考えると、

構造体のサイズ1
struct size_test_1 {
    int a;
    int b;
    int c;
    char d[100];
};

int main(void) {
    struct size_test_1 x;
    printf("%u\n", sizeof(struct size_test_1));
}

実行時に表示される struct size_test_1 構造体のサイズはおそらく 112 になると思います。

これは int 型のサイズが 4 バイト、char 型のサイズが 1 バイトで、さらに struct size_test_1 構造体が int 型のメンバが 3 つ、char 型の要素数 100 の配列 をメンバを持つため、サイズが下記式により計算されるためです。

4 * 3 + 1 * 100 = 112

アライメントを考慮した構造体のサイズの計算方法

ただし、厳密に言うと、構造体の各メンバのメモリ上への配置位置(アドレス)はメンバの型に応じて調整が行われます。

このアドレスの調整が行われるのはコンピュータが動作しやすいようにするためで、C言語の場合はコンパイラが勝手にやってくれます。

MEMO

構造体のメンバだけでなく、通常の型の変数にもアドレスの調整が適用されます

が、通常の型の変数の場合、特にこのアドレスの調整を意識する必要はないです

ただし、構造体の場合は型のサイズに影響するので特に注意が必要です

また、この調整をアライメントと言います。

アライメントというのは「1列に並べる」などを意味する単語で、構造体のメンバにおいては、メンバの型に応じて各メンバのメモリ上の配置位置(アドレス)を調整することをアライメントと言います。

例えば「変数 x を4バイトアライメントする」といった場合、変数 x は4バイトの倍数のアドレスに配置することを意味ます。

例えば構造体の各メンバにおいては、型に応じて下記のようにアライメントされるのが一般的です。

  • char:1バイトアライメント
  • short:2バイトアライメント
  • int:4バイトアライメント
  • ポインタ:8バイトアライメント(CPU によっては4バイトの可能性もあり)

さらに、構造体のサイズは各メンバの中の最大アライメントサイズの倍数に切り上げられるはずです(おそらく…)。

これを踏まえた上で、次の構造体のサイズについて考えてみましょう。

構造体のサイズ2
struct size_test_2 {
    char a;
    int b;
    char c;
    short d;
    char e[5];
};

int main(void) {
    struct size_test_2 x;
    printf("%u\n", sizeof(struct size_test_2));
}

実行するとおそらく struct size_test_2 型のサイズとして 20 が表示されると思います。

単純に各メンバのサイズを足し合わせて考えると下記の式により 13 バイトになりますよね?

1 + 4 + 1 + 2 + 1 * 5 = 13

なのに、なぜサイズが 20 バイトになるのか、struct size_test_2 型の変数 x の先頭アドレスを仮に 0x1000 (16進数)として考えてみましょう。

構造体において、各メンバは基本的に、上側のものから順に(アドレスの小さい方から)メモリ上に配置されます。

ですので変数 x のメンバ a はまず 0x1000 に配置されます。a のサイズは1バイトですので、1バイト分 a 用にメモリが確保されます。

1つ目のメンバの配置位置

次に配置されるのはメンバ b です。メンバ b は int 型ですので4バイトアライメントで配置されます。つまりメンバ b は a が配置された直後のアドレスである 0x1001 ではなく、4の倍数に切り上げされた 0x1004 に配置されます。int 型のサイズは4バイトですので、0x1004 〜 0x1007 がメンバ b 用のメモリとなります。

2つ目のメンバの配置

この時、0x1001 〜 0x1003 の3バイト分は空きになります。このようなアドレス調整用に発生した空きをパディングと呼びます。

パディング挿入の様子

次に配置されるのはメンバ c は char 型ですので1バイトアライメントで配置されることになりますが、メンバ b 用のメモリの直後のアドレス 0x1008 は当然1バイトの倍数ですので 0x1008 に配置されます。

3つ目のメンバの配置

次のメンバ d は short 型ですので2バイトアライメントで配置されることになります。メンバ c 用のメモリの直後のアドレス 0x1009 は2バイトの倍数ではないので、次の2バイトの倍数である 0x100A に配置されます。 

short 型のサイズは2バイトですので、0x100A 〜 0x100B がメンバ d 用のメモリとなります。

4つ目のメンバの配置

ここでは1バイト分のパディングが発生しています。

さらにメンバ e は配列ではあるものの  char 型ですので1バイトアライメントが適用され、メンバ d 用のメモリの直後 0x100C に配置されます。メンバ e は char 型の要素数 5 の配列ですので 0x100C から5バイト分、つまり 0x100C 〜 0x1011 に配置されることになります。

5つ目のメンバの配置

ここまで解説してきた内容を踏まえると、struct size_test_2 型の変数 x のメンバは下記のように配置されることになります。

  • x.achar):0x1000 〜 0x1000
  • x.bint):0x1004 〜 0x1007
  • x.cchar):0x1008 〜 0x1008
  • x.dshort):0x100A 〜 0x100B
  • x.echarの配列):0x100C 〜 0x1011

つまり、struct size_test_2 型の変数 x のメンバはメモリ上の 0x1000 〜 0x1011 に配置されていることになります。単純に計算すると10進数で 17バイトになります。

が、最終的に構造体のサイズはメンバの中で最大のアライメントサイズに応じてサイズも切り上げされますので、struct size_test_2 型のサイズは4バイトの倍数に切り上げられ、最終的に struct size_test_2 型の変数 x のサイズは20バイトということになります(この際にも3バイト分のパディングが発生します)。

構造体そのもののサイズ調整によるパディング

こんな感じで構造体のサイズはアライメントを考慮したバイト数として計算されます。

構造体のサイズを削減する工夫

前述の通り、アライメントにより構造体にはパディングが発生し、その分構造体のサイズが大きくなることになります。

構造体のサイズが大きくなると、構造体の変数用のメモリサイズが大きくなりますし、構造体のコピーなどを行うとサイズが大きくなった場合にその分処理時間が長くなります。

少しでも構造体のサイズを小さくしたい場合は、構造体のメンバの並びを変更して改善できないかどうかを検討してみましょう!

アライメントを考慮した構造体のサイズの計算方法で紹介した struct size_test_2 のメンバの並び順を変更した下記の struct size_test_3 のサイズを考えてみましょう。

構造体のサイズ3
struct size_test_3 {
    char a;
    char c;
    short d;
    int b;
    char e[5];
};

int main(void) {
    struct size_test_3 x;
    printf("%u\n", sizeof(struct size_test_2));
}

struct size_test_3 ではパディングができるだけ発生しないようにメンバの並び順を工夫しています。

各メンバのメモリ配置を図示すると下のようになります。

メンバの並び順を工夫した時のメモリ配置

例えばメンバ d はメンバ a とメンバ c の合計サイズが2バイトなので、パディングを行わなくても2バイトアライメントされた位置に配置することができます。

さらにメンバ b においても、メンバ a とメンバ c とメンバ d の合計サイズが4バイトなので、パディングを行わなくても4バイトアライメントされた位置に配置することができます。

こんな感じでパディングのサイズが減るように配置してやることで、構造体のサイズを小さくすることも可能です。

構造体の変数をたくさん使用したり、構造体のコピーがたくさん発生する場合は効果的です。

MEMO

環境によって上記で示したバイト数は異なる場合があります

またコンパイラの設定等により型に限らずアライメントのサイズを1バイトに設定することなども可能なようです

が、基本的な考えは、ここまで説明してきた内容であると考えていただいて良いです

スポンサーリンク

構造体名の再定義

ここまで構造体を利用する場合は必ず struct 構造体タグ名 という形で記述してきましたが、typedef を利用すれば struct を省略して構造体を利用できるようになります。

typedef とは

typedef は「元々ある型(例えば char や unsinged int)」に「別の新しい名前」をつける命令になります。

typedefの使い方
typedef 元々ある型 新しい型名;

例えば、下記のように typedef 命令を実行すれば、unsigned int の代わりに新しく名付けた u32 を型名として使用できるようになります。

typedefの使用例
typedef unsigned int u32;
int main(void) {
    int a = -1;
    u32 b, c;
    b = (u32)a;
}

変数宣言だけでなくキャストなどでも使用できます。

サイズや符号の有無は typedef 命令で指定した 元々ある型 と同じになります。

typedef による構造体の型名の再定義

この typedef 命令は構造体の型名にも利用できます。

例えば、下記のように typedef 命令を実行すれば、struct student の代わりに STUDENT_T を型名として使用できるようになります。

毎回 struct を記述する必要がなくなるので楽になると思います。

構造体の型名の再定義例1
struct student {
    char age; /* 年齢 */
    int number; /* 学生番号 */
    char name[100]; /* 名前 */
};
typedef struct student STUDENT_T;

下記のようにして構造体定義と typedef を同時に行うことも可能です。

構造体の型名の再定義例2
typedef struct student {
    char age; /* 年齢 */
    int number; /* 学生番号 */
    char name[100]; /* 名前 */
} STUDENT_T;

さらに、下記のようにして構造体タグ名を省略して typedef を行うことも可能です。

構造体の型名の再定義例3
typedef struct {
    char age; /* 年齢 */
    int number; /* 学生番号 */
    char name[100]; /* 名前 */
} STUDENT_T;

スポンサーリンク

メンバに自身の構造体のポインタを持たせたい場合

もし構造体内で、自身の構造体のポインタのメンバを持たせたい場合は、下記のように事前に typedef してやれば良いです。

構造体の型名の再定義例4
typedef struct student STUDENT_T;
struct student {
    char age; /* 年齢 */
    int number; /* 学生番号 */
    char name[100]; /* 名前 */
    STUDENT_T *lover; /* 好きな生徒 */
};

実は ↑ のように構造体内に自身の構造体のポインタをメンバとして持たせることは多いです。例えばリスト構造体やツリー構造体などはこのような形式で構造体を定義することが多いです。

構造体のメリット

 

ソースコードを読むのが楽

まず一つ目のメリットはソースコードが読みやすくなるという点です。

構造体は「関連する複数のデータを1つにまとめて管理する型」であり、データの関連性が分かりやすく、そのためソースコードが読みやすくなります。

例えば下記のソースコードを見てみると、変数 agenumbername がただ並んでいるだけなので、これらの変数の関連性が分かりにくく、ソースコードを読み進めてどのような関連があるかを判断していく必要があります。 

構造体なしのソースコード
int main(void) {
    char age;
    int number;
    char name[200];

    age = 18;
    number = 1021;
    strcpy(name, "TARO");
    
    return 0;
}

一方で、下記のように構造体を利用すれば、x.agex.numberx.name が、ある生徒(x)の情報であることが一目で分かります。

つまり各データの関係性が分かりやすく、ソースコードも読みやすくなります。

構造体ありのソースコード
struct student {
    char age; /* 年齢 */
    int number; /* 学生番号 */
    char name[100]; /* 名前 */
};

int main(void) {
    struct student x;

    x.age = 18;
    x.number = 1021;
    strcpy(x.name, "TARO");

    return 0;
}

スポンサーリンク

ソースコードを書くのが楽

また、変数宣言やデータのコピー、関数定義や関数呼び出し時に書くソースコードの量を削減することも可能です。

例えば構造体なしで記述した下記のソースコードでは、変数宣言や変数のコピー、関数呼び出しや関数定義時に指定する引数の数はデータの数の分だけ記述する必要があります。

構造体なしのソースコード
void printStudentInfo(char, int, char *);

void printStudentInfo(char age, int number, char *name) {
    printf("name = %s\n", name);
    printf("age = %d\n", age);
    printf("number = %d\n", number);
}

int main(void) {
    /* 変数宣言 */
    char x_age;
    int x_number;
    char x_name[100];

    char y_age;
    int y_number;
    char y_name[100];

    x_age = 18;
    x_number = 1021;
    strcpy(x_name, "TARO");

    /* データのコピー */
    y_age = x_age;
    y_number = x_number;
    strcpy(y_name, x_name);

    /* 関数呼び出し */
    printStudentInfo(y_age, y_number, y_name);

    return 0;
}

一方で、構造体を使った場合、構造体の単位で変数宣言やコピー、関数呼び出しや関数定義時の引数の指定が行えますので、メンバの数が増えてもこれらの記述に必要なソースコード量は増えません。

そのため構造体を用いることによりソースコードの書きやすさも向上すると考えられると思います(構造体の定義やメンバにアクセスするための記述が増えたりもしますが…)。

構造体ありのソースコード
struct student {
    char age; /* 年齢 */
    int number; /* 学生番号 */
    char name[100]; /* 名前 */
};

void printStudentInfo(struct student *);

void printStudentInfo(struct student *x) {
    printf("name = %s\n", x->name);
    printf("age = %d\n", x->age);
    printf("number = %d\n", x->number);
}

int main(void) {
    /* 変数宣言 */
    struct student x, y;

    x.age = 18;
    x.number = 1021;
    strcpy(x.name, "TARO");

    /* データのコピー */
    y = x;

    /* 関数呼び出し */
    printStudentInfo(&y);

    return 0;
}

プログラムを拡張するのが楽

構造体のメリットの中で一番重要なのがこのプログラムの拡張しやすさだと思います。

例えばソースコードが書きやすいで紹介したプログラムに対し「生徒の身長を表示する機能」を追加することを考えてみましょう。

構造体を利用しない場合は下記のようにソースコードを変更する必要があります。変更部分は黄色背景で示しています。

構造体なしのソースコード
void printStudentInfo(char, int, char *, double);

void printStudentInfo(char age, int number, char *name, double height) {
    printf("name = %s\n", name);
    printf("age = %d\n", age);
    printf("number = %d\n", number);
    printf("height = %.1f\n", height);
}

int main(void) {
    /* 変数宣言 */
    char x_age;
    int x_number;
    char x_name[100];
    double x_height;

    char y_age;
    int y_number;
    char y_name[100];
    double y_height;

    x_age = 18;
    x_number = 1021;
    strcpy(x_name, "TARO");
    x_height = 175.3;

    /* データのコピー */
    y_age = x_age;
    y_number = x_number;
    strcpy(y_name, x_name);
    y_height = x_height;

    /* 関数呼び出し */
    printStudentInfo(y_age, y_number, y_name, y_height);

    return 0;
}

一方で構造体を利用している場合の変更部分は下記の黄色背景部分になります。

構造体ありのソースコード
struct student {
    char age; /* 年齢 */
    int number; /* 学生番号 */
    char name[100]; /* 名前 */
    double height; /* 身長 */
};

void printStudentInfo(struct student *);

void printStudentInfo(struct student *x) {
    printf("name = %s\n", x->name);
    printf("age = %d\n", x->age);
    printf("number = %d\n", x->number);
    printf("height = %.1f\n", x->height);
}

int main(void) {
    /* 変数宣言 */
    struct student x, y;

    x.age = 18;
    x.number = 1021;
    strcpy(x.name, "TARO");
    x.height = 175.3;

    /* データのコピー */
    y = x;

    /* 関数呼び出し */
    printStudentInfo(&y);

    return 0;
}

見比べていただければわかると思いますが、構造体を利用した方が黄色背景で示した変更点が少ないです。

特に重要なポイントは、構造体ありの場合は「関数の引数の変更なし」で拡張できている点です。

引数を変更してしまうと呼び出し元の部分を全てを修正する必要があります。つまり関数の呼び出し元が多ければ多いほど、引数変更時に修正する箇所が多くなり、影響範囲が広くなってしまうことになります。

構造体を利用すれば、引数の変更なしに、メンバの追加だけで拡張することができる場合があります。

このように、構造体を用いると拡張時のソースコードの変更量を少なくでき、特に影響範囲が大きくなる可能性のある引数の変更を防ぐことができる(ケースがある)ため、拡張しやすくなるというメリットがあります。

MEMO

構造体を用いれば必ず引数の変更がなくなるというわけではないので注意してください

構造体は「関連する複数のデータを1つにまとめて管理する型」であり、関連しないデータはメンバに追加しない方が良いです

したがって、構造体に関連しないデータ部分を拡張する場合は、メンバ追加ではなく引数追加などが必要になります

構造体を使う上での注意点

最後に構造体を使う上での注意点について簡単に説明しておきます。

スポンサーリンク

何でもかんでも構造体のメンバに追加しない

構造体は便利なので、ついついメンバを追加してしまいがちです。

あくまでも構造体は「関連する複数のデータを1つにまとめて管理する型」ですので、関連しないデータはメンバに追加しないようにしましょう。

逆にデータの関連性が分かりにくくなり、ソースコードが読みにくくなってしまいます。

ポインタのメンバがコピーされるのはアドレスのみ

特に構造体のメンバにポインタがある場合の注意点です。

構造体以外のポインタでも同様ですが、メンバにポインタがある場合、構造体をコピーしてもそのメンバに対してコピーが行われるのはアドレスのみです。

ポインタの指しているメモリ領域が新たに作成されてコピーされるわけではないので注意しましょう。

例えば下記では、x.ay.b は同じアドレスを指しているので、x.a の値を変更すると y.b の値も変わることになります。

アドレスのコピー
#include <stdio.h>
#include <stdlib.h>


struct test {
    int *a;
};

int main(void) {
    struct test x, y;

    x.a = (int *)malloc(sizeof(int));
    *(x.a) = 100;

    y = x;

    printf("y.a = %d\n", *(y.a)); /* 100が表示される */
    
    *(x.a) = 200;


    printf("y.a = %d\n", *(y.a));  /* 200が表示される */

    free(x.a);

    return 0;
}

まとめ

このページではC言語の構造体について解説しました。

構造体は「関連する複数のデータを1つにまとめて管理する型」です。

基本的に他の型同様に使うことができますが、メンバを持つため、メンバへのアクセスを行いながらデータの格納や取得を行う必要があります。

構造体を利用することで下記のようなメリットがあります。

  • ソースコードを読むのが楽
  • ソースコードを書くのが楽
  • プログラムを拡張するのが楽

是非このページで構造体について理解し、どしどし使い込んで構造体になれていきましょう!

ただし、何でもかんでも構造体にメンバを追加するとかえってソースコードが読みにくくなります。

構造体は「関連する複数のデータを1つにまとめて管理する型」であることをしっかり認識しておき、関連するデータのみをメンバに持たせるようにしましょう!

オススメの参考書(PR)

C言語学習中だけど分からないことが多くて挫折しそう...という方には、下記の「スッキリわかるC言語入門」がオススメです!

まず学習を進める上で、参考書は2冊持っておくことをオススメします。この理由は下記の2つです。

  • 参考書によって、解説の仕方は異なる
  • 読み手によって、理解しやすい解説の仕方は異なる

ある人の説明聞いても理解できなかったけど、他の人からちょっと違った観点での説明を聞いて「あー、そういうことね!」って簡単に理解できた経験をお持ちの方も多いのではないでしょうか?

それと同じで、1冊の参考書を読んで理解できない事も、他の参考書とは異なる内容の解説を読むことで理解できる可能性があります。

なので、参考書は2冊持っておいた方が学習時に挫折しにくいというのが私の考えです。

特に上記の「スッキリわかるC言語入門」は、他の参考書とは違った切り口での解説が豊富で、他の参考書で理解できなかった内容に対して違った観点での解説を読むことができ、オススメです。題名の通り「なぜそうなるのか?」がスッキリ理解できるような解説内容にもなっており、C言語入門書としてもかなり分かりやすい参考書だと思います。

もちろんネット等でも色んな観点からの解説を読むことが出来ますので、分からない点は別の人・別の参考書の解説を読んで解決していきましょう!もちろん私のサイトも参考にしていただけると嬉しいです!

入門用のオススメ参考書は下記ページでも紹介していますので、こちらも是非参考にしていただければと思います。

https://daeudaeu.com/c_reference_book/

同じカテゴリのページ一覧を表示