C言語のきほん|構造体の活用方法

構造体を組み合わせると、データのつながりが見えやすくなり、C言語の設計がぐっと上手になる。

これまでの内容では、構造体を使って異なる型のデータをひとまとまりにする基本を学んできました。
構造体が使えるようになると、名前・番号・点数のような関連する情報を整理して扱えるようになり、プログラムがかなり見やすくなります。

ただ、実際のプログラムでは、1つの構造体だけで表しきれないデータもたくさんあります。
たとえば「人の情報」の中に「生年月日」があり、「商品の情報」の中に「発売日」があり、「予定の情報」の中に「日時」があるように、ひとつのまとまりの中に、さらに別のまとまりを入れたくなる場面が出てきます。

そんなときに便利なのが、構造体の入れ子です。
これは、構造体のメンバとして、別の構造体を持たせる方法です。

構造体を入れ子にすると、単にデータを並べるだけでなく、意味のある単位ごとに整理して表現できるようになります。
たとえば、年・月・日をバラバラに持つのではなく、Date という1つの型にまとめてから Person の中に入れることで、「これは生年月日を表すデータなんだ」とすぐに分かるようになります。

この考え方は、あとで関数や配列、ポインタと組み合わせるときにもとても役立ちます。
ここでは、構造体の入れ子の基本的な考え方、宣言の順序、初期化の方法、メンバの参照方法まで、やさしく丁寧に整理していきましょう。

構造体を活用するとはどういうことか

構造体の基本では、異なる型のデータを1つにまとめる方法を学びました。
たとえば、氏名と年齢をひとまとめにして「学生情報」にする、といった考え方です。

ここからさらに一歩進むと、構造体の活用方法として次のような発展が見えてきます。

活用方法内容
構造体を入れ子にする構造体の中に別の構造体を入れる
構造体同士を代入する同じ型の構造体同士で値をまとめてコピーする
メンバへ入力するscanf などを使って値を受け取る

今回の内容では、その最初の重要なテーマである構造体の入れ子を中心に見ていきます。

構造体の入れ子とは何か

構造体の入れ子とは、構造体の中に別の構造体をメンバとして含めることです。

たとえば、人の情報を考えてみましょう。
人には名前があります。
そして、生年月日もあります。

ここで、生年月日を

  • year
  • month
  • day

という3つの int 型メンバとして Person の中に直接書くこともできます。
でも、それでは「この3つは1組の日付なんだ」というまとまりが見えにくくなります。

そこで、まず日付だけを表す Date という構造体を作り、それを Person の中に入れます。

このようにすると、

  • Person は「人の情報」
  • Date は「日付情報」

という役割がはっきり分かれます。

なぜ構造体を入れ子にするのか

構造体を入れ子にする大きな理由は、関連するデータを意味ごとに整理できるからです。

たとえば、次の2つを比べてみてください。

日付を直接書いた場合

typedef struct {
    char name[50];
    int year;
    int month;
    int day;
} Person;

日付を構造体にまとめた場合

typedef struct {
    int year;
    int month;
    int day;
} Date;

typedef struct {
    char name[50];
    Date birth;
} Person;

後者の方が、「birth は日付情報を表している」とすぐ分かります。
このように、構造体を入れ子にすると、データの意味が整理されて読みやすくなります。

入れ子にするメリット

利点内容
意味のまとまりが分かりやすいyear, month, day が1つの日付だと分かる
可読性が上がるメンバの役割を理解しやすい
保守しやすい日付に関する定義を Date 側でまとめられる
再利用しやすいDate 型を他の構造体でも使える

入れ子にする構造体は先に宣言する

ここはとても大事なポイントです。
構造体の中に別の構造体をメンバとして入れるときは、入れ子として使われる構造体を先に宣言しておく必要があります

たとえば、Person の中で Date を使いたいなら、先に Date を定義しておかなければなりません。

正しい順序は次の通りです。

typedef struct {
    int year;
    int month;
    int day;
} Date;

typedef struct {
    char name[50];
    Date birth;
} Person;

もし Date より先に Person を書いてしまうと、その時点では Date という型をまだ知らないため、エラーになります。

構造体の入れ子は、図にするととても分かりやすいです。

この図では、Person の中に birth というメンバがあり、その birth 自体が Date 型の構造体であることを表しています。
つまり、birth は単なる1つの数値ではなく、year・month・day をまとめた日付情報です。
このように、構造体を入れ子にすると、複雑なデータでも意味のまとまりを保ったまま整理できます。

サンプルプログラムで確認する

ここでは、書籍情報と発売日の例に説明します。

ファイル名:15_5_1.c

#include <stdio.h>

// 発売日を表す構造体
typedef struct {
    int year;    // 年
    int month;   // 月
    int day;     // 日
} ReleaseDate;

// 書籍情報を表す構造体
typedef struct {
    char title[50];        // 書名
    ReleaseDate release;   // 発売日
} Book;

int main(void)
{
    // 構造体の入れ子を使って初期化
    Book book = {"C言語しっかり入門", {2024, 4, 12}};

    printf("書名:%s\n", book.title);
    printf("発売日:%d年%d月%d日\n",
           book.release.year, book.release.month, book.release.day);

    return 0;
}

実行結果例

書名:C言語しっかり入門
発売日:2024年4月12日

サンプルプログラムを順に読み解く

まず、発売日を表す ReleaseDate 型を定義しています。

typedef struct {
    int year;
    int month;
    int day;
} ReleaseDate;

この構造体は、日付に必要な情報をひとまとめにしています。
年・月・日を、単なるバラバラの変数としてではなく、「日付」という意味のある単位で扱えるようにしているわけです。

次に、書籍情報を表す Book 型を定義しています。

typedef struct {
    char title[50];
    ReleaseDate release;
} Book;

ここでの release メンバが、入れ子になっている構造体です。
型は ReleaseDate なので、release の中には year、month、day が入っています。

つまり Book は、

  • title
  • release.year
  • release.month
  • release.day

という情報を持つ構造になっています。

入れ子の構造体を初期化する方法

入れ子の構造体を初期化するときは、内側の構造体に対応する部分を波括弧で囲むと分かりやすくなります。

Book book = {"C言語しっかり入門", {2024, 4, 12}};

この初期化を分解すると、

  • title に C言語しっかり入門
  • release.year に 2024
  • release.month に 4
  • release.day に 12

が入ります。

なぜ波括弧で囲むのか

入れ子になっている部分を明確にするためです。
見た目にも、「ここが release 構造体の中身ですよ」と分かりやすくなります。

表で整理する

初期化子対応するメンバ
"C言語しっかり入門"title
{2024, 4, 12}release
2024release.year
4release.month
12release.day

入れ子が深くなるほど、波括弧でまとまりを意識して書くことが大切になります。

入れ子の構造体メンバを参照する方法

入れ子の構造体のメンバを参照するときは、ドット演算子を続けて使います

たとえば、

book.release.year

は、

  • book の中の
  • release の中の
  • year

という意味です。

同じように、

book.release.month
book.release.day

とも書けます。

参照の流れを分解して考える

書き方意味
bookBook 型の変数全体
book.releaseその中の ReleaseDate 型メンバ
book.release.yearさらにその中の year メンバ

このように、外側から順番にたどっていくイメージで読むと分かりやすいです。

ドット演算子が2つ必要になる理由

通常の構造体なら、

student.id

のようにドット演算子は1つでした。
でも入れ子構造体では、「構造体の中の構造体の中のメンバ」にアクセスするので、段階が1つ増えます。

たとえば、

book.release.year

では、

  1. book の中の release を取り出す
  2. release の中の year を取り出す

という2段階の操作をしているため、ドット演算子も2つ必要になります。

これは難しい文法というより、データの階層が深くなった分だけ、アクセスの道筋も長くなっていると考えると自然です。

入れ子にしない場合との比較

入れ子の便利さをよりはっきり感じるために、入れ子にしない場合と比べてみましょう。

入れ子にしない書き方

typedef struct {
    char title[50];
    int release_year;
    int release_month;
    int release_day;
} Book;

この書き方でも必要な情報は持てます。
ただ、release_year、release_month、release_day が「発売日」というひとつのまとまりであることが少し見えにくくなります。

入れ子にした書き方

typedef struct {
    int year;
    int month;
    int day;
} ReleaseDate;

typedef struct {
    char title[50];
    ReleaseDate release;
} Book;

こちらの方が、発売日がひとつの独立したまとまりとして表現されています。

比較表

観点入れ子なし入れ子あり
書きやすさ単純少し階層が増える
意味のまとまりやや見えにくいとても分かりやすい
再利用性低いReleaseDate を他でも使える
保守性項目が増えると整理しにくい役割ごとに整理しやすい

再利用できるのも大きな強み

入れ子にするために作った ReleaseDate 型は、Book だけでなく他の構造体にも使えます。

たとえば、

  • イベントの開催日
  • 商品の入荷日
  • 会員の登録日

などにも同じ日付型を使えます。

このように、意味のある小さな構造体を先に作っておくと、あとで別の場面でも再利用しやすくなります。
これが、構造体を部品のように組み合わせる考え方です。

入れ子構造体では、メンバ参照の書き方で少し混乱することがあります。
そこで、参照の流れを図で見えるようにすると理解しやすくなります。

この図では、book.release.year という記述が、ただ長い名前なのではなく、構造体を順番にたどっていることを表しています。
最初に book の中の release を見て、次にその release の中の year を見ています。
この流れが分かると、入れ子構造体のメンバ参照はぐっと理解しやすくなります。

よくある注意点

構造体の入れ子では、最初のうちに引っかかりやすいポイントがあります。

入れ子にする構造体を後から書こうとしてしまう

これはできません。
先に型を知られていないと、メンバとして使えないからです。
必ず、内側で使う構造体を先に宣言します。

メンバ参照でドットが1つ足りない

たとえば、

book.year

とは書けません。
year は Book の直接のメンバではなく、release の中にあるからです。
正しくは、

book.release.year

です。

初期化の波括弧を省略して読みにくくしてしまう

入れ子部分を波括弧で明示しておくと、どこがどの構造体に対応しているのかが分かりやすくなります。
特に学習中は、入れ子部分をきちんと波括弧で囲んで書くのがおすすめです。

実践的に考えるとどんな場面で使うか

構造体の入れ子は、実際にはかなり幅広い場面で使えます。

外側の構造体内側の構造体
人物情報PersonDate
商品情報ProductDate
予定情報ScheduleTime
住所録PersonAddress

たとえば住所も、

  • 郵便番号
  • 都道府県
  • 市区町村
  • 番地

を Address 型としてまとめておけば、Person の中に Address address; として自然に組み込めます。

このように、構造体の入れ子は「現実の情報のまとまり」をそのままプログラムの形にしやすくする仕組みとも言えます。

この段階で押さえておきたいポイント

ここまでの内容で、特に大切な点を整理しておきます。

ポイント内容
構造体の入れ子とは構造体の中に別の構造体をメンバとして含めること
入れ子の利点意味ごとにデータを整理できる
宣言の順序内側で使う構造体を先に宣言する
初期化方法入れ子部分を波括弧で囲うと分かりやすい
メンバ参照外側から順にドット演算子でたどる
再利用性小さな構造体を別の構造体でも使い回せる

構造体を入れ子にできるようになると、単純なデータの集まりだけでなく、少し複雑な情報もきれいに表現できるようになります。
C言語で「現実のまとまり」をそのままコードに落とし込む感覚がつかめてくる、とても大切なステップです。