C言語基礎|構造体を使ったデータ設計

「Point を作ったら、次は Car。構造体の組み合わせで“現実の設計”がそのままコードになる!」

構造体を覚えると、ただ値をまとめるだけじゃなくて、現実のモノをどう表すか(データ設計)ができるようになります。
ここがめちゃくちゃ楽しいところです。

現実の「自動車」を考えると、必要な情報はいくつかあります。
たとえば「現在位置」と「燃料」。
さらに「現在位置」は、X座標とY座標のセットですよね。

つまり、

  • 座標は Point(x と y)という“まとまり”
  • 自動車は Car(pt と fuel)という“まとまり”
    のように、構造体を部品として組み立てるのが自然です。

この記事では、この “部品化して組み立てる” という視点で、構造体を使ったデータ設計をやさしく整理します。
そして最後に、同じ考え方を使ったシンプルなプログラム例も動かして確認します。

現実を分解して、構造体で組み立てる

まずは「何を部品として切り出すか」がデータ設計の第一歩です。

現実世界 → プログラムの部品

現実の概念プログラムでの部品中身
座標Pointx, y
自動車Carpt(Point), fuel

説明

  • 座標はいつでも x と y がセットなので、Point として独立させると再利用しやすいです。
  • 自動車は「位置」と「燃料」のセットなので、Car としてまとめると意味がハッキリします。

メンバ と 構成メンバ(分解の考え方)

ここが設計として面白いポイントです。
Car のメンバは2個なのに、分解すると“部品の数”は増えます。

図:Car のメンバと構成メンバ

Car
  メンバ:pt と fuel

pt は Point なので中身がある
  pt.x
  pt.y

構成メンバ(これ以上分解できない単位)
  pt.x, pt.y, fuel

Car の数え方

種類内容個数
メンバpt, fuel2
構成メンバpt.x, pt.y, fuel3

説明

  • pt は“まとまり”としてのメンバ
  • pt.x と pt.y は最小単位(構成メンバ)
  • どこまで分解して考えるかで、設計の見通しが良くなります

ドット演算子が2段になる(入れ子アクセス)

Car の位置は pt に入っていて、pt の中に x と y があるので、アクセスがこうなります。

アクセス例(超定番)

書き方意味
c.pt自動車 c の現在位置(Point)
c.fuel自動車 c の残り燃料
c.pt.x現在位置のX座標
c.pt.y現在位置のY座標

図:c.pt.x の読み方

c(Car)
 └ pt(Point)
     └ x(X座標)

説明
ドット演算子は左から順に結びつく(左結合)ので、c.pt.x は (c.pt).x と同じ意味です。
そのため、普段は括弧なしで書けます。

ポインタで受けると -> が混ざる(更新する関数の定番)

自動車を移動させるような関数は、内部で Car の中身を更新したいので、Car へのポインタを受け取る形が定番です。

書式(ポインタ先のメンバアクセス)

p->mem

意味

  • p が指す構造体のメンバ mem を表します

. と -> の使い分け

対象記法
構造体変数.c.fuel
構造体へのポインタ->cptr->fuel

サンプルプログラム

仕様

  • Point と Car を定義(Car は Point をメンバに持つ)
  • 移動先 dest までの距離を使って燃料が減る。
  • 燃料不足なら移動しない。
  • 結果を表示する。

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

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

#include <math.h>
#include <stdio.h>

#define sqr(n) ((n) * (n))

typedef struct {
    double x;
    double y;
} Point;

typedef struct {
    Point  pt;     // 現在位置
    double fuel;   // 残り燃料(距離と同じだけ減る)
} Car;

double distance_of(Point a, Point b)
{
    return sqrt(sqr(a.x - b.x) + sqr(a.y - b.y));
}

int move_to(Car *c, Point dest)
{
    double d = distance_of(c->pt, dest);
    if (d > c->fuel) {
        return 0; // 移動失敗
    }

    c->pt = dest;
    c->fuel -= d;
    return 1; // 移動成功
}

int main(void)
{
    Car mycar = {{0.0, 0.0}, 10.0};
    Point dest = {6.0, 8.0};

    printf("移動前:位置(%.1f, %.1f)  燃料%.1f\n",
           mycar.pt.x, mycar.pt.y, mycar.fuel);

    if (move_to(&mycar, dest)) {
        printf("移動しました。\n");
    } else {
        printf("燃料が足りず移動できません。\n");
    }

    printf("移動後:位置(%.1f, %.1f)  燃料%.1f\n",
           mycar.pt.x, mycar.pt.y, mycar.fuel);

    return 0;
}

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

Point と Car の宣言(データ設計の核)

書式(typedef を使った構造体型定義)

typedef struct {
    ...
} 型名;

何をする命令?

  • 構造体型を定義して、型名を1単語で使えるようにします。

今回の型設計

型名役割メンバ
Point座標を表す部品x, y
Car自動車を表す本体pt(Point), fuel

説明
Point を部品化しておくと、Car 以外(例:目的地、チェックポイント、地図の点)にも使い回せます。

distance_of(距離を計算する関数)

書式

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

今回:

double distance_of(Point a, Point b)

何をする命令?

  • 2点 a と b の距離を求めて返します。

図:2点間距離のイメージ

説明
座標の差(x差、y差)から、三平方の形で距離を出しています。

move_to(Car を更新する関数)

書式

int move_to(Car *c, Point dest)

何をする命令?

  • c が指す自動車を dest へ移動させます(移動できるなら)
  • 移動距離分だけ燃料を減らします
  • 成功なら 1、失敗なら 0 を返します

move_to の処理手順

手順処理使う要素
1距離 d を求めるdistance_of(c->pt, dest)
2燃料が足りるか確認d > c->fuel
3位置を更新c->pt = dest
4燃料を更新c->fuel -= d

図:更新されるメンバ

移動先を代入:c->pt = dest
燃料を減らす:c->fuel -= d

説明
Car の中身を変えるので、引数は Car *c です。
ここで -> を使うことで、ポインタ先のメンバをスッキリ書けます。

データ設計としての学び(ここが本題)

この例のポイントは、プログラムのテクニックというより 設計の分け方です。

設計のコツ

コツ内容この例での対応
まとまりは部品にする再利用できる単位で切るPoint を独立させる
本体は部品を持つ現実の形に合わせるCar が Point を持つ
更新が必要ならポインタ状態を変える関数の定番move_to(Car *c, ...)
参照だけなら値渡しでもOK読むだけなら安全distance_of(Point a, Point b)

演習問題

演習12-5

上のプログラムを改造して、移動の指定方法を次の2通りから選べるようにしてください。

  • 1:目的地の座標を入力する(dest.x と dest.y を入力)
  • 2:現在地からの移動量を入力する(dx と dy を入力し、dest = 現在地 + 移動量 で目的地を作る)

例:現在地が (5.0, 3.0) で (7.5, 8.9) に行きたい

  • 座標入力なら 7.5 と 8.9
  • 移動量入力なら 2.5 と 5.9

解答例

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

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

#include <math.h>
#include <stdio.h>

#define sqr(n) ((n) * (n))

typedef struct { double x, y; } Point;
typedef struct { Point pt; double fuel; } Car;

double distance_of(Point a, Point b)
{
    return sqrt(sqr(a.x - b.x) + sqr(a.y - b.y));
}

int move_to(Car *c, Point dest)
{
    double d = distance_of(c->pt, dest);
    if (d > c->fuel) return 0;

    c->pt = dest;
    c->fuel -= d;
    return 1;
}

int main(void)
{
    Car mycar = {{5.0, 3.0}, 20.0};
    Point dest;
    int mode;

    printf("現在地(%.1f, %.1f) 燃料%.1f\n", mycar.pt.x, mycar.pt.y, mycar.fuel);
    printf("移動方法を選択(1:座標  2:移動量):");
    scanf("%d", &mode);

    if (mode == 1) {
        printf("目的地X:"); scanf("%lf", &dest.x);
        printf("目的地Y:"); scanf("%lf", &dest.y);
    } else {
        double dx, dy;
        printf("移動量dx:"); scanf("%lf", &dx);
        printf("移動量dy:"); scanf("%lf", &dy);
        dest.x = mycar.pt.x + dx;
        dest.y = mycar.pt.y + dy;
    }

    if (move_to(&mycar, dest)) {
        printf("移動しました。\n");
    } else {
        printf("燃料が足りず移動できません。\n");
    }

    printf("現在地(%.1f, %.1f) 燃料%.1f\n", mycar.pt.x, mycar.pt.y, mycar.fuel);
    return 0;
}

解説(ポイント)

  • mode で入力方法を切り替え。
  • 移動量入力では dest を計算で作る(現在地+移動量)
  • move_to は同じまま使える(設計が分離できている証拠)