C言語 構造体(struct)の練習問題

構造体を使う問題を、初級から上級まで11問そろえました。多くの問題では構造体の定義と main 関数の一部ができあがっていて、指定された部分を書きます。「採点する」を押すと、本物の gcc でいくつかの入力を試し、出力が正しいかを判定します。

この練習問題の使い方
  1. 問題文と実行例を読み、下の欄にコードを書く
  2. 「▶ 実行」で自分で入力を与えて動かしてみる
  3. 「✔ 採点する」で、用意したいくつかの入力に対して出力が正しいかを判定
  4. 行き詰まったら「ヒント」、解けたら「模範解答と解説」で確認
採点のルール
  • 出力は実行例と同じ形で表示する。「数を入力してください」のような入力を促すメッセージは表示しない
  • 行末の空白と、最後の空行の有無は区別しない
  • 書いたコードはこのブラウザに自動保存されます

※ 実行と採点では、コードを外部の無料コンパイルサービス Wandbox に送信して gcc で動かします。

全11問(初級 3・中級 5・上級 3)

構造体の練習問題(11問)

T01 初級

問1. 点の構造体

座標を表す構造体 struct point(メンバ x, y)が定義されている。整数 x, y(各 −10000 以上 10000 以下)を読み込んで構造体変数 p のメンバに入れ、「(x, y)」の形で表示し、次の行に原点からの距離の2乗(x² + y²)を表示せよ。
実行例(下線が入力)
3 4 (3, 4) 原点からの距離の2乗: 25
コードを書く
「実行」で入力を与えて動かす/「採点する」で 4 個のテストを自動で試します
メンバは p.x のようにドット(.)でアクセスします。scanf には普通の変数と同じく &p.x を渡します(. の方が & より先に結びつくので、&(p.x) の意味になります)。
#include <stdio.h>

struct point {
    int x;
    int y;
};

int main(void) {
    struct point p;

    scanf("%d %d", &p.x, &p.y);
    printf("(%d, %d)\n", p.x, p.y);
    printf("原点からの距離の2乗: %d\n", p.x * p.x + p.y * p.y);
    return 0;
}
解説:構造体は関係のある複数の値を1つにまとめる型です。struct point までが型名なので、変数宣言では struct を省略できません(省略したいときは typedef を使います)。構造体の定義の最後の }; のセミコロンを忘れやすいので注意しましょう。
T02 初級

問2. 学生1人分のデータ

名前と点数を持つ構造体 struct student が定義されている。名前(空白を含まない英字、1〜19文字)と点数(0〜100)を読み込んで構造体変数 s に入れ、名前・点数・判定(60点以上なら「合格」、それ未満は「不合格」)を表示せよ。
実行例(下線が入力)
sato 72 名前: sato 点数: 72 判定: 合格
コードを書く
「実行」で入力を与えて動かす/「採点する」で 5 個のテストを自動で試します
名前は文字列なので scanf("%19s", s.name) のように & を付けません(配列名がそのまま先頭アドレスになるため)。点数は &s.score です。
#include <stdio.h>

struct student {
    char name[20];
    int score;
};

int main(void) {
    struct student s;

    scanf("%19s %d", s.name, &s.score);
    printf("名前: %s\n", s.name);
    printf("点数: %d\n", s.score);
    if (s.score >= 60) {
        printf("判定: 合格\n");
    } else {
        printf("判定: 不合格\n");
    }
    return 0;
}
解説:構造体のメンバには配列(文字列)も置けます。%19s の 19 は「最大19文字まで読む」という指定で、'\0' の分を残して配列(20要素)からあふれないようにしています。文字列のメンバに s.name = "sato"; と代入はできない点にも注意(初期化以外では strcpy を使います)。
T03 初級

問3. 時刻を秒に直す関数

時・分・秒を持つ構造体 struct time を値渡しで受け取り、0時0分0秒からの通算秒数を返す関数 int to_seconds(struct time t) を書け。main 関数はできあがっている(時(0〜23)・分(0〜59)・秒(0〜59)を読み込み、「hh:mm:ss = 秒数秒」の形で表示する)。
実行例(下線が入力)
1 2 3 01:02:03 = 3723秒
コードを書く
「実行」で入力を与えて動かす/「採点する」で 4 個のテストを自動で試します
1時間は 3600 秒、1分は 60 秒です。関数の中ではメンバを t.h, t.m, t.s として使えます。
#include <stdio.h>

struct time {
    int h;
    int m;
    int s;
};

int to_seconds(struct time t) {
    return t.h * 3600 + t.m * 60 + t.s;
}

int main(void) {
    struct time t;
    scanf("%d %d %d", &t.h, &t.m, &t.s);
    printf("%02d:%02d:%02d = %d秒\n", t.h, t.m, t.s, to_seconds(t));
    return 0;
}
解説:構造体は int と同じように値渡しできます。このとき関数には構造体全体のコピーが渡るので、関数の中で t.h = 0; としても main の t は変わりません。読むだけの関数なら値渡しで十分です。%02d は「幅2、足りなければ 0 で埋める」指定です。
T04 中級

問4. 構造体配列で成績表

typedef で名前を付けた構造体 Student(名前と点数)の配列に、N 人(1 ≤ N ≤ 50)のデータが読み込まれている(main の前半はできあがっている)。続きを書いて、平均点を小数第1位まで表示し、次の行に最高点の人の名前と点数を表示せよ。最高点が複数いるときは、先に入力された人を表示する。
実行例(下線が入力)
3 sato 72 ito 95 kato 64 平均点: 77.0 最高点: ito (95点)
コードを書く
「実行」で入力を与えて動かす/「採点する」で 5 個のテストを自動で試します
最高点の人は「点数」ではなく添字 best で覚えておくと、名前も一緒に取り出せます。best = 0 から始め、s[i].score > s[best].score のときだけ更新します(>= にすると後の人に変わります)。
#include <stdio.h>

typedef struct {
    char name[20];
    int score;
} Student;

int main(void) {
    int n;
    Student s[50];

    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        scanf("%19s %d", s[i].name, &s[i].score);
    }

    int sum = 0, best = 0;
    for (int i = 0; i < n; i++) {
        sum += s[i].score;
        if (s[i].score > s[best].score) {
            best = i;
        }
    }
    printf("平均点: %.1f\n", (double)sum / n);
    printf("最高点: %s (%d点)\n", s[best].name, s[best].score);
    return 0;
}
解説:typedef struct { ... } Student; とすると、struct を書かずに Student だけで型名として使えます。構造体配列の要素のメンバは s[i].score のように「配列の添字 → ドット」の順で書きます。平均は (double)sum / n と片方を double にしないと、整数の割り算で小数が切り捨てられます。
T05 中級

問5. ポインタ渡しで口座を更新

口座を表す構造体 Account(名義と残高)をポインタで受け取って書き換える2つの関数を書け。
・void deposit(Account *a, int amount):残高を amount 増やす
・int withdraw(Account *a, int amount):残高が足りれば amount 減らして 1 を返し、足りなければ何もせず 0 を返す
main 関数はできあがっている(名義・初期残高を読み、続いて操作の回数 Q と、Q 行の「+ 金額」(入金)または「- 金額」(出金)を読んで、操作ごとに結果を表示する)。金額・初期残高は 0 以上 100000 以下、Q は 1 以上 20 以下。
実行例(下線が入力)
sato 1000 3 + 500 - 2000 - 1500 sato の残高: 1500 残高不足 sato の残高: 1500 sato の残高: 0
コードを書く
「実行」で入力を与えて動かす/「採点する」で 4 個のテストを自動で試します
ポインタ a が指す構造体のメンバは a->balance と書きます((*a).balance と同じ意味)。出金は if (a->balance < amount) return 0; で先に足りないケースを抜けると書きやすいです。
#include <stdio.h>

typedef struct {
    char name[20];
    int balance;
} Account;

void deposit(Account *a, int amount) {
    a->balance += amount;
}

int withdraw(Account *a, int amount) {
    if (a->balance < amount) {
        return 0;
    }
    a->balance -= amount;
    return 1;
}

int main(void) {
    Account acc;
    int q;
    scanf("%19s %d", acc.name, &acc.balance);
    scanf("%d", &q);
    for (int i = 0; i < q; i++) {
        char op;
        int amount;
        scanf(" %c %d", &op, &amount);
        if (op == '+') {
            deposit(&acc, amount);
        } else if (!withdraw(&acc, amount)) {
            printf("残高不足\n");
        }
        printf("%s の残高: %d\n", acc.name, acc.balance);
    }
    return 0;
}
解説:構造体を値渡し(void deposit(Account a, int amount))にすると、書き換わるのはコピーだけで main の残高は増えません。書き換えたいときはポインタで渡し、構造体変数なら .、構造体へのポインタなら -> と使い分けます。a.balance と書くとコンパイルエラーになり、*a.balance は *(a.balance) と解釈されるので、括弧付きの (*a).balance か a->balance にします。
T06 中級

問6. 2点の中点を返す関数

座標を表す構造体 Point(double 型のメンバ x, y)を2つ受け取り、その中点を構造体として返す関数 Point midpoint(Point a, Point b) を書け。main 関数はできあがっている(2点の座標 x1 y1 x2 y2(各 −1000 以上 1000 以下の整数)を読み込み、中点を小数第1位まで表示する)。
実行例(下線が入力)
0 0 3 4 中点: (1.5, 2.0)
コードを書く
「実行」で入力を与えて動かす/「採点する」で 4 個のテストを自動で試します
m.x = (a.x + b.x) / 2; のように、メンバごとに計算して m に入れ、return m; で返します。
#include <stdio.h>

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

Point midpoint(Point a, Point b) {
    Point m = {0.0, 0.0};
    m.x = (a.x + b.x) / 2;
    m.y = (a.y + b.y) / 2;
    return m;
}

int main(void) {
    Point a, b;
    scanf("%lf %lf %lf %lf", &a.x, &a.y, &b.x, &b.y);
    Point m = midpoint(a, b);
    printf("中点: (%.1f, %.1f)\n", m.x, m.y);
    return 0;
}
解説:関数は return で値を1つしか返せませんが、構造体なら複数のメンバをまとめて返せます。返した構造体は呼び出し側にコピーされるので、ローカル変数 m を返しても問題ありません(ローカル変数のアドレスを返すのとは違います)。メンバが int だと (0 + 3) / 2 が 1 になってしまうので、ここでは double にしています。double を scanf で読むときは %lf です。
T07 中級

問7. 3科目の成績(構造体の中の配列)

名前と3科目の点数の配列を持つ構造体 Student がある。1人分の合計点と平均点を表示する関数 void print_report(const Student *s) を書け。出力は1人1行で「名前 合計 平均」(平均は小数第1位まで)。main 関数はできあがっている(N(1 ≤ N ≤ 20)と、N 行の「名前 点数1 点数2 点数3」(点数は 0〜100)を読み込み、1人ずつ print_report を呼ぶ)。
実行例(下線が入力)
2 sato 80 70 92 ito 100 100 99 sato 242 80.7 ito 299 99.7
コードを書く
「実行」で入力を与えて動かす/「採点する」で 3 個のテストを自動で試します
配列メンバの要素は s->score[j] と書きます。for (int j = 0; j < 3; j++) で合計を出し、平均は total / 3.0 で求めます。
#include <stdio.h>

typedef struct {
    char name[20];
    int score[3];
} Student;

void print_report(const Student *s) {
    int total = 0;
    for (int j = 0; j < 3; j++) {
        total += s->score[j];
    }
    printf("%s %d %.1f\n", s->name, total, total / 3.0);
}

int main(void) {
    int n;
    Student st[20];
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        scanf("%19s", st[i].name);
        for (int j = 0; j < 3; j++) {
            scanf("%d", &st[i].score[j]);
        }
    }
    for (int i = 0; i < n; i++) {
        print_report(&st[i]);
    }
    return 0;
}
解説:構造体が大きいときは、値渡しでまるごとコピーする代わりにポインタで渡すのが一般的です。読むだけなら const Student * にしておくと、うっかり書き換えたときにコンパイルエラーで気づけます。total / 3 と整数で割ると小数が切り捨てられ、さらに int を %.1f に渡すことになるので表示がおかしくなります。
T08 中級

問8. 長方形の中に点があるか(構造体の入れ子)

点 Point(int の x, y)と、左下の角 corner(Point 型)・幅 w・高さ h を持つ長方形 Rect が定義されている(構造体の中に構造体がある)。点 p が長方形の内側(辺の上を含む)にあれば 1、なければ 0 を返す関数 int contains(const Rect *r, Point p) を書け。内側とは、x が corner.x 以上 corner.x + w 以下、かつ y が corner.y 以上 corner.y + h 以下であること。main 関数はできあがっている(長方形の x y w h、点の個数 Q、Q 個の点を読み込み、点ごとに「内側」か「外側」を表示する)。座標は −1000 以上 1000 以下、w, h は 1 以上 1000 以下。
実行例(下線が入力)
0 0 4 3 3 1 1 4 3 5 0 (1, 1): 内側 (4, 3): 内側 (5, 0): 外側
コードを書く
「実行」で入力を与えて動かす/「採点する」で 4 個のテストを自動で試します
入れ子のメンバは順につないで書きます。ポインタ r からなら r->corner.x です(r はポインタなので ->、corner は構造体そのものなので .)。4つの条件を && でつなぎます。
#include <stdio.h>

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

typedef struct {
    Point corner;
    int w;
    int h;
} Rect;

int contains(const Rect *r, Point p) {
    return r->corner.x <= p.x && p.x <= r->corner.x + r->w &&
           r->corner.y <= p.y && p.y <= r->corner.y + r->h;
}

int main(void) {
    Rect r;
    int q;
    scanf("%d %d %d %d", &r.corner.x, &r.corner.y, &r.w, &r.h);
    scanf("%d", &q);
    for (int i = 0; i < q; i++) {
        Point p;
        scanf("%d %d", &p.x, &p.y);
        printf("(%d, %d): %s\n", p.x, p.y, contains(&r, p) ? "内側" : "外側");
    }
    return 0;
}
解説:-> と . は「左側がポインタなら ->、構造体なら .」で決まります。r->corner は Point 型の構造体なので、その先は .x です。r->corner->x と書くとエラーになります。比較の結果(0 か 1)はそのまま return できます。辺の上を含むので <= を使います。
T09 上級

問9. 成績表を点数順に並べ替え

N 人(1 ≤ N ≤ 50)の名前と点数を、点数の高い順に並べ替える関数 void sort_by_score(Student *s, int n) を書け。同じ点数の人は入力された順のままにすること。main 関数はできあがっている(データを読み込み、並べ替えたあとに「順位 名前 点数」を1行ずつ表示する。順位は単に 1 から数える)。
実行例(下線が入力)
4 sato 72 ito 95 kato 72 abe 88 1 ito 95 2 abe 88 3 sato 72 4 kato 72
コードを書く
「実行」で入力を与えて動かす/「採点する」で 4 個のテストを自動で試します
バブルソートで、隣どうしを比べて s[j].score < s[j + 1].score のときだけ入れ替えます。構造体は Student tmp = s[j]; s[j] = s[j + 1]; s[j + 1] = tmp; のようにまるごと代入できます。
#include <stdio.h>

typedef struct {
    char name[20];
    int score;
} Student;

void sort_by_score(Student *s, int n) {
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - 1 - i; j++) {
            if (s[j].score < s[j + 1].score) {
                Student tmp = s[j];
                s[j] = s[j + 1];
                s[j + 1] = tmp;
            }
        }
    }
}

int main(void) {
    int n;
    Student s[50];
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        scanf("%19s %d", s[i].name, &s[i].score);
    }
    sort_by_score(s, n);
    for (int i = 0; i < n; i++) {
        printf("%d %s %d\n", i + 1, s[i].name, s[i].score);
    }
    return 0;
}
解説:構造体どうしの代入 = では、メンバ(中の文字配列 name も含めて)が全部コピーされます。点数だけを入れ替えると名前と点数の組がずれてしまうので、構造体ごと入れ替えます。比較を <= にすると同点でも入れ替わり、同点の人の順が入力順でなくなります(テスト4)。隣どうしを「より小さいときだけ」入れ替えるバブルソートは、同じ値の順番を保つ安定なソートです。
T10 上級

問10. 分数の足し算と約分

分子 num と分母 den を持つ構造体 Fraction がある。次の2つの関数を書け(最大公約数を求める gcd は用意されている)。
・Fraction reduce(Fraction f):約分した分数を返す。分母は正にし、分子が 0 なら 0/1 にする
・Fraction add(Fraction a, Fraction b):a + b を約分して返す
main 関数はできあがっている(分数 a, b を「分子 分母 分子 分母」の順に読み込み、「a/b + c/d = 結果」の形で表示する。入力の分数は約分せずにそのまま表示する)。分子は −1000 以上 1000 以下、分母は 1 以上 1000 以下。結果の分母が 1 でも「2/1」の形で表示する。
実行例(下線が入力)
1 2 1 3 1/2 + 1/3 = 5/6
コードを書く
「実行」で入力を与えて動かす/「採点する」で 6 個のテストを自動で試します
通分すると a/b + c/d = (a×d + c×b) / (b×d) です。add ではこれを c に入れて return reduce(c); とします。reduce では g = gcd(f.num, f.den) を求めて分子・分母を g で割ります(分子が 0 なら gcd は分母そのものになり、結果は 0/1 になります)。
#include <stdio.h>

typedef struct {
    int num;
    int den;
} Fraction;

int gcd(int a, int b) {
    if (a < 0) a = -a;
    if (b < 0) b = -b;
    while (b != 0) {
        int r = a % b;
        a = b;
        b = r;
    }
    return a;
}

Fraction reduce(Fraction f) {
    if (f.den < 0) {
        f.num = -f.num;
        f.den = -f.den;
    }
    int g = gcd(f.num, f.den);
    f.num /= g;
    f.den /= g;
    return f;
}

Fraction add(Fraction a, Fraction b) {
    Fraction c = {0, 1};
    c.num = a.num * b.den + b.num * a.den;
    c.den = a.den * b.den;
    return reduce(c);
}

int main(void) {
    Fraction a, b;
    scanf("%d %d %d %d", &a.num, &a.den, &b.num, &b.den);
    Fraction c = add(a, b);
    printf("%d/%d + %d/%d = %d/%d\n", a.num, a.den, b.num, b.den, c.num, c.den);
    return 0;
}
解説:reduce は構造体を値渡しで受け取り、直したものを返す形です。受け取った f はコピーなので書き換えても呼び出し元には影響せず、return f; で返した値だけが使われます。そのため reduce(c); と呼んだだけで戻り値を使わないと、約分されません(c = reduce(c); か return reduce(c); とします)。この問題の入力では分母は正なので符号の調整は念のための処理ですが、負の分数を扱うときは「分母を正にそろえる」と決めておくと表示が安定します。
T11 上級

問11. 在庫管理

商品 Item(名前・単価・在庫数)の構造体配列を使って、注文を処理する2つの関数を書け。
・Item *find_item(Item *items, int n, const char *name):名前が name と一致する商品へのポインタを返す。なければ NULL
・int order(Item *items, int n, const char *name, int qty):注文を1件処理し、売上金額を返す。商品がなければ「名前: 商品がありません」と表示して 0 を返す。在庫が qty 未満なら「名前: 在庫不足 (残り k 個)」と表示して 0 を返す。在庫が足りれば在庫を減らし、「名前: qty 個 x 単価 円 = 金額 円」と表示して金額を返す
main 関数はできあがっている(商品数 N(1 ≤ N ≤ 20)と N 行の「名前 単価 在庫」、注文数 Q(1 ≤ Q ≤ 20)と Q 行の「名前 個数」を読み込み、最後に売上合計と各商品の残りを表示する)。単価は 1〜10000、在庫・個数は 0〜100。
実行例(下線が入力)
3 apple 120 10 milk 200 2 bread 150 5 4 apple 3 milk 5 egg 1 milk 2 apple: 3 個 x 120 円 = 360 円 milk: 在庫不足 (残り 2 個) egg: 商品がありません milk: 2 個 x 200 円 = 400 円 売上合計: 760 円 apple 残り 7 個 milk 残り 0 個 bread 残り 5 個
コードを書く
「実行」で入力を与えて動かす/「採点する」で 4 個のテストを自動で試します
文字列の比較は == ではなく strcmp(items[i].name, name) == 0 を使います。find_item では一致したら &items[i](items + i でも同じ)を返します。order では Item *it = find_item(items, n, name); で受け取り、it->stock や it->price を使います。
#include <stdio.h>
#include <string.h>

typedef struct {
    char name[20];
    int price;
    int stock;
} Item;

Item *find_item(Item *items, int n, const char *name) {
    for (int i = 0; i < n; i++) {
        if (strcmp(items[i].name, name) == 0) {
            return &items[i];
        }
    }
    return NULL;
}

int order(Item *items, int n, const char *name, int qty) {
    Item *it = find_item(items, n, name);
    if (it == NULL) {
        printf("%s: 商品がありません\n", name);
        return 0;
    }
    if (it->stock < qty) {
        printf("%s: 在庫不足 (残り %d 個)\n", name, it->stock);
        return 0;
    }
    it->stock -= qty;
    int amount = it->price * qty;
    printf("%s: %d 個 x %d 円 = %d 円\n", name, qty, it->price, amount);
    return amount;
}

int main(void) {
    int n, q;
    Item items[20];
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        scanf("%19s %d %d", items[i].name, &items[i].price, &items[i].stock);
    }

    int total = 0;
    scanf("%d", &q);
    for (int i = 0; i < q; i++) {
        char name[20];
        int qty;
        scanf("%19s %d", name, &qty);
        total += order(items, n, name, qty);
    }

    printf("売上合計: %d 円\n", total);
    for (int i = 0; i < n; i++) {
        printf("%s 残り %d 個\n", items[i].name, items[i].stock);
    }
    return 0;
}
解説:find_item が返すのはmain の配列の要素を指すポインタなので、it->stock -= qty; で本物の在庫が減ります。もし Item it = items[i]; と構造体をコピーして在庫を減らすと、コピーが変わるだけで配列の在庫は減りません。NULL が返ったときに it->stock を読むと異常終了の原因になるので、必ず先に確かめます。items[i].name == name はアドレスの比較なので、同じ文字列でも一致しません。

復習する講座

← 練習問題の一覧へ