C言語基礎|構造体をメンバに持つ構造体

「データは“入れ子”で強くなる。構造体をメンバに持たせて、現実の形にもっと近づけよう!」

構造体に慣れてくると、次にやりたくなるのが「もっと現実っぽい形でデータを表したい」です。
現実のモノって、だいたい“部品”を持っていますよね。

たとえば「配送先」には住所があって、住所には郵便番号や都道府県があって…みたいに、
まとまりの中に、さらにまとまりがあることが多いです。

C言語では、こういう“入れ子”の表現ができます。
つまり、構造体のメンバとして構造体を持てるんです。

これができると、

  • コードの意味が自然になる。
  • メンバの関連が崩れにくい。
  • まとまり単位で処理できて読みやすい
    と、いいことがたくさん出てきます。

まず結論:構造体のメンバは構造体でもOK

書式(構造体をメンバにする)

struct Outer {
    struct Inner inner;
    ...
};

または typedef を使って、もっと読みやすくできます。

typedef struct { ... } Inner;

typedef struct {
    Inner inner;
    ...
} Outer;

「住所(Address)」を構造体で表し、それを「配送ラベル(Label)」のメンバとして持たせます。

構造体をメンバに持つイメージ(図で理解)

図:Label の中に Address が入っている(入れ子)

図の説明

  • addr は「住所1つ分のまとまり」
  • zip/city/street が住所の部品
  • Label の中に Address が“丸ごと”入るので、現実の構造に近いです

サンプルプログラム(構造体メンバとして構造体を使う)

仕様

  • Address(郵便番号・市区町村・番地)を構造体で表す
  • Label(宛先名・住所)を構造体で表す(住所は Address をメンバにする)
  • 入力して、整形表示する

プロジェクト名:chap12-8-1 ソースファイル名:chap12-8-1.c

Visual Studio でこのプログラムを実行するには、SDLチェック設定を変更しておく必要があります。
1.プロジェクト名を右クリックして、「プロパティ」をクリックします。
2.「C/C++」→「全般」→「SDLチェック」を「いいえ」に切り替えて「OK」をクリックします。

#include <stdio.h>

#define NAME_LEN   32
#define ZIP_LEN     8
#define CITY_LEN   32
#define STREET_LEN 64

typedef struct {
    char zip[ZIP_LEN];         // 郵便番号(例:123-4567)
    char city[CITY_LEN];       // 市区町村
    char street[STREET_LEN];   // 番地など
} Address;

typedef struct {
    char to[NAME_LEN];   // 宛先名
    Address addr;        // 住所(構造体をメンバにする)
} Label;

void print_label(Label x)
{
    printf("\nラベルを作成しました。\n");
    printf("宛先:%s\n", x.to);
    printf("〒%s\n", x.addr.zip);
    printf("%s %s\n", x.addr.city, x.addr.street);
}

int main(void)
{
    Label l;

    printf("宛先名を入力:");
    scanf("%s", l.to);

    printf("郵便番号を入力:");
    scanf("%s", l.addr.zip);

    printf("市区町村を入力:");
    scanf("%s", l.addr.city);

    printf("番地を入力:");
    scanf("%s", l.addr.street);

    print_label(l);

    return 0;
}

このプログラムで登場する各項目を表で詳しく解説

typedef struct { ... } Address;

書式

typedef struct {
    ...
} Address;

何をする?

  • 構造体型を定義し、それに Address という型名(typedef名)を付けます。
  • 以後、Address だけで型として使えます。

表:Address のメンバ

メンバ意味
zipchar配列郵便番号
citychar配列市区町村
streetchar配列番地など

説明
住所という“まとまり”は、zip/city/street のセットなので、Address としてまとめるのが自然です。

typedef struct { ... } Label;

書式

typedef struct {
    char to[...];
    Address addr;
} Label;

何をする?

  • Label という型を作ります。
  • そして addr というメンバに Address 型を入れています(ここが主役!)。

Label のメンバ

メンバ意味
tochar配列宛先名
addrAddress住所(構造体のまとまり)

説明
Label は「宛先名」+「住所」という構造になっていて、住所は Address としてひとまとめに持ちます。

メンバアクセス(ドット演算子)が2段になる

構造体の中に構造体があると、アクセスがこうなります。

アクセス例

書き方意味
l.toラベルの宛先名
l.addrラベルの住所(Address ひとまとまり)
l.addr.zip住所の郵便番号
l.addr.city住所の市区町村
l.addr.street住所の番地

図:l.addr.zip の読み方

l(Label)
 └ addr(Address)
     └ zip(郵便番号)

図の説明
「Label の addr の zip」という順番で辿ります。
これが“入れ子構造”をそのままコードに落とした形です。

関数に構造体を渡す(print_label の役割)

書式(関数定義)

戻り値型 関数名(引数...)
{
    ...
}

今回の print_label はこうでした。

void print_label(Label x)

何をする?

  • Label x を受け取り、ラベルを整形して表示します。
  • 表示だけが目的なので戻り値は void です。

print_label の中で使う要素

要素何をする?
printf画面へ文字列を表示する
x.addr.zip など入れ子メンバにアクセスして表示する

入力での注意:char配列は & が不要

scanf は基本的に「書き込み先の住所」が必要です。
ただし char配列は配列名が先頭要素を指すように扱われるので、そのまま渡します。

scanf と & の要不要

入力先&
char配列l.to不要
char配列l.addr.zip不要
int など&value必要

こういうときに「構造体メンバの構造体」が効く

効果が出る場面

場面なぜ便利?
住所・座標・日付のような“部品まとまり”を複数の場所で使うAddress や Point を再利用できる
関数の引数をスッキリさせたいAddress を1つ渡すだけで済む
関連のある項目がズレるのを防ぎたいzip/city/street を常に一体として扱える

まとめ:入れ子構造は“現実に近い設計”

  • 構造体のメンバは、基本型だけでなく配列や構造体でもOK
  • 構造体をメンバに持つと、現実の「部品のまとまり」を自然に表現できる。
  • アクセスはドット演算子が2段になる(l.addr.zip のように辿る)
  • まとまりが強くなって、コードが読みやすく、ミスもしにくくなる。