C言語基礎|構造体の配列

「1人分をまとめたら、次は“みんな分”。構造体の配列でデータ管理が一気に実戦レベル!」

構造体で「1人分」「1商品分」みたいに“まとまり”を作れるようになると、次にやりたくなるのは…そう、複数人分をまとめて扱うことです。

たとえば「商品を10個管理したい」「センサーを5台分まとめたい」みたいな場面では、
構造体を1個ずつ用意していると管理が大変になってきます。

そこで登場するのが 構造体を要素にした配列、つまり 構造体の配列です。
配列なので、添字でアクセスできて、ループで回せて、ソートもできます。
しかも、1要素が “データのまとまり” なので、関連性が崩れにくくてとても素直です。

構造体の配列とは?(まずはイメージ)

図:配列の1要素が「カード1枚」

図の説明

  • 配列の各要素が、構造体(カード)1枚ぶん
  • だから items[i] で「i番目の商品(まとまり)」を取り出せます

なぜ“構造体の配列”が便利なの?

ばらばらの配列で管理すると、対応関係が壊れやすいです。

ばらばら管理 vs まとめ管理

管理方法起きがちな問題
ばらばらの配列names[], prices[], stocks[]並び替えでズレやすい、同じ番号が本当に同じ商品か不安
構造体の配列items[] の各要素に name/price/stock1要素が1商品なのでズレない、ソートも素直

サンプルプログラム

元の「学生を身長順ソート」ではなく、商品リストを価格順にソートする例に変更します。
表示メッセージも別の日本語に置き換えています。

仕様

  • 商品(名前・価格・在庫)を構造体で表す
  • 商品を複数個、構造体の配列に入れる
  • 価格の昇順にソートして表示する
  • 交換は swap_Product で「構造体丸ごと」入れ替える

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

#include <stdio.h>

#define COUNT 5
#define NAME_LEN 32

typedef struct {
    char   name[NAME_LEN];  // 商品名
    int    price;           // 価格(円)
    int    stock;           // 在庫数
} Product;

// x と y が指す Product を交換する
void swap_Product(Product *x, Product *y)
{
    Product temp = *x;
    *x = *y;
    *y = temp;
}

// 配列 a の先頭 n 個を価格の昇順にソートする(バブルソート)
void sort_by_price(Product a[], int n)
{
    for (int i = 0; i < n - 1; i++) {
        for (int j = n - 1; j > i; j--) {
            if (a[j - 1].price > a[j].price) {
                swap_Product(&a[j - 1], &a[j]);
            }
        }
    }
}

int main(void)
{
    Product items[] = {
        {"りんご",   158, 12},
        {"みかん",   120, 30},
        {"バナナ",   198,  8},
        {"メロン",   980,  2},
        {"ぶどう",   398,  5},
    };

    printf("ソート前の一覧です。\n");
    for (int i = 0; i < COUNT; i++) {
        printf("%-8s %4d円  在庫%2d\n",
               items[i].name, items[i].price, items[i].stock);
    }

    sort_by_price(items, COUNT);

    printf("\n価格順に並べ替えました。\n");
    for (int i = 0; i < COUNT; i++) {
        printf("%-8s %4d円  在庫%2d\n",
               items[i].name, items[i].price, items[i].stock);
    }

    return 0;
}

構造体の配列の宣言と初期化(書式と意味)

書式:構造体配列の宣言

型名 配列名[要素数];

書式:初期化子でまとめて入れる

型名 配列名[] = {
    { ... }, { ... }, ...
};

今回の例の読み方

記述意味
Product items[]Product 型の要素が並ぶ配列 items
{"りんご", 158, 12}1要素分(1商品のまとまり)を初期化
items[i].pricei番目の商品(items[i])の price メンバ

メンバアクセス:配列×構造体の書き方

構造体配列ではこの形が定番です。

よく使う形

書き方意味
items[i]i番目の構造体(商品1つ)
items[i].namei番目の商品の名前
items[i].pricei番目の商品の価格
items[i].stocki番目の商品の在庫

図:items[i] は「1商品カード」

交換関数 swap_Product がラクな理由

構造体の配列を扱うとき、交換(入れ替え)は超よく出ます。
ここで「構造体は代入できる」という性質が効いてきます。

交換の考え方

方法何を交換する?大変さ
ばらばら管理名前配列も価格配列も在庫配列も全部交換ミスりやすい
構造体配列Product を丸ごと交換1回で済む

swap_Product の中身(読み方)

Product temp = *x;
*x = *y;
*y = temp;
  • x と y は Product へのポインタ
  • *x と *y は「その場にある Product 実体」
  • 代入で丸ごとコピーできるので、交換がスッキリします

ソート関数 sort_by_price の見どころ

この例のソートは「バブルソート」系の形です。

書式:配列を受け取る関

void sort_by_price(Product a[], int n)

何をする命令?

  • Product の配列 a の先頭 n 個を、価格の昇順に並べ替えます。
  • a[] という形は「配列を受け取っている」ように見えますが、実際には 先頭要素へのポインタとして扱われます。
  • そのため、関数内で a の要素を入れ替えると、呼び出し側の配列も並び替わります。

図:関数は配列の先頭を受け取って並べ替える

main の items[0] ──→ sort_by_price の a[0]
main の items[1] ──→ sort_by_price の a[1]
...
(同じ配列を見ている)

派生型(derived type)の話をやさしく整理

C言語は「型を組み合わせて新しい型を作る」のが得意です。
構造体の配列は、まさに “構造体” と “配列” の合体ですね。

代表的な派生型(今回の範囲で押さえたい)

派生型何を作る?イメージ
配列型同じ要素型がずらっと並ぶa[10]
構造体型いろいろな型をひとまとめstruct
ポインタ型どこかを指す型int *p
関数型引数と戻り値で決まる型int f(int)

図:構造体の配列は「派生の合体」

Product(構造体型)
   ↓ を要素にして
Product items[5](配列型)

演習問題

演習12-4

上の「商品を価格順にソートするプログラム」を、次のように書き換えてください。

  • 商品データは初期化子で与えず、キーボードから読み込む
  • 価格の昇順にソートするか、名前の昇順にソートするかを選べるようにする
    (選択は 1:価格 / 2:名前 のような入力でOK)

解答例(そのまま動く版)

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

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

#define COUNT 5
#define NAME_LEN 32

typedef struct {
    char name[NAME_LEN];
    int  price;
    int  stock;
} Product;

void swap_Product(Product *x, Product *y)
{
    Product temp = *x;
    *x = *y;
    *y = temp;
}

void sort_by_price(Product a[], int n)
{
    for (int i = 0; i < n - 1; i++) {
        for (int j = n - 1; j > i; j--) {
            if (a[j - 1].price > a[j].price) {
                swap_Product(&a[j - 1], &a[j]);
            }
        }
    }
}

void sort_by_name(Product a[], int n)
{
    for (int i = 0; i < n - 1; i++) {
        for (int j = n - 1; j > i; j--) {
            if (strcmp(a[j - 1].name, a[j].name) > 0) {
                swap_Product(&a[j - 1], &a[j]);
            }
        }
    }
}

int main(void)
{
    Product items[COUNT];

    for (int i = 0; i < COUNT; i++) {
        printf("商品%dの名前:", i + 1);
        scanf("%s", items[i].name);

        printf("商品%dの価格:", i + 1);
        scanf("%d", &items[i].price);

        printf("商品%dの在庫:", i + 1);
        scanf("%d", &items[i].stock);
    }

    int mode;
    printf("\n並べ替え方法を選んでください(1:価格  2:名前):");
    scanf("%d", &mode);

    if (mode == 1) {
        sort_by_price(items, COUNT);
        printf("\n価格順に並べ替えました。\n");
    } else {
        sort_by_name(items, COUNT);
        printf("\n名前順に並べ替えました。\n");
    }

    for (int i = 0; i < COUNT; i++) {
        printf("%-8s %4d円  在庫%2d\n",
               items[i].name, items[i].price, items[i].stock);
    }

    return 0;
}

解説(ポイント)

  • 構造体の配列 items[i] に対して、メンバごとに scanf で読み込む。
  • sort_by_name では strcmp を使って文字列比較(辞書順)
  • 並べ替えの本体は「比較して swap」で共通なので、構造体丸ごと交換が効いてきます。