構造体を使う問題を、初級から上級まで11問そろえました。多くの問題では構造体の定義と main 関数の一部ができあがっていて、指定された部分を書きます。「採点する」を押すと、本物の gcc でいくつかの入力を試し、出力が正しいかを判定します。
※ 実行と採点では、コードを外部の無料コンパイルサービス Wandbox に送信して gcc で動かします。
struct point(メンバ x, y)が定義されている。整数 x, y(各 −10000 以上 10000 以下)を読み込んで構造体変数 p のメンバに入れ、「(x, y)」の形で表示し、次の行に原点からの距離の2乗(x² + y²)を表示せよ。「実行」で入力を与えて動かす/「採点する」で 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; }
struct point までが型名なので、変数宣言では struct を省略できません(省略したいときは typedef を使います)。構造体の定義の最後の }; のセミコロンを忘れやすいので注意しましょう。struct student が定義されている。名前(空白を含まない英字、1〜19文字)と点数(0〜100)を読み込んで構造体変数 s に入れ、名前・点数・判定(60点以上なら「合格」、それ未満は「不合格」)を表示せよ。「実行」で入力を与えて動かす/「採点する」で 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 を使います)。struct time を値渡しで受け取り、0時0分0秒からの通算秒数を返す関数 int to_seconds(struct time t) を書け。main 関数はできあがっている(時(0〜23)・分(0〜59)・秒(0〜59)を読み込み、「hh:mm:ss = 秒数秒」の形で表示する)。「実行」で入力を与えて動かす/「採点する」で 4 個のテストを自動で試します
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; }
t.h = 0; としても main の t は変わりません。読むだけの関数なら値渡しで十分です。%02d は「幅2、足りなければ 0 で埋める」指定です。typedef で名前を付けた構造体 Student(名前と点数)の配列に、N 人(1 ≤ N ≤ 50)のデータが読み込まれている(main の前半はできあがっている)。続きを書いて、平均点を小数第1位まで表示し、次の行に最高点の人の名前と点数を表示せよ。最高点が複数いるときは、先に入力された人を表示する。「実行」で入力を与えて動かす/「採点する」で 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 にしないと、整数の割り算で小数が切り捨てられます。Account(名義と残高)をポインタで受け取って書き換える2つの関数を書け。void deposit(Account *a, int amount):残高を amount 増やすint withdraw(Account *a, int amount):残高が足りれば amount 減らして 1 を返し、足りなければ何もせず 0 を返す+ 金額」(入金)または「- 金額」(出金)を読んで、操作ごとに結果を表示する)。金額・初期残高は 0 以上 100000 以下、Q は 1 以上 20 以下。「実行」で入力を与えて動かす/「採点する」で 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 にします。Point(double 型のメンバ x, y)を2つ受け取り、その中点を構造体として返す関数 Point midpoint(Point a, Point b) を書け。main 関数はできあがっている(2点の座標 x1 y1 x2 y2(各 −1000 以上 1000 以下の整数)を読み込み、中点を小数第1位まで表示する)。「実行」で入力を与えて動かす/「採点する」で 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 です。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 を呼ぶ)。「実行」で入力を与えて動かす/「採点する」で 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 に渡すことになるので表示がおかしくなります。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 以下。「実行」で入力を与えて動かす/「採点する」で 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 できます。辺の上を含むので <= を使います。void sort_by_score(Student *s, int n) を書け。同じ点数の人は入力された順のままにすること。main 関数はできあがっている(データを読み込み、並べ替えたあとに「順位 名前 点数」を1行ずつ表示する。順位は単に 1 から数える)。「実行」で入力を与えて動かす/「採点する」で 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)。隣どうしを「より小さいときだけ」入れ替えるバブルソートは、同じ値の順番を保つ安定なソートです。num と分母 den を持つ構造体 Fraction がある。次の2つの関数を書け(最大公約数を求める gcd は用意されている)。Fraction reduce(Fraction f):約分した分数を返す。分母は正にし、分子が 0 なら 0/1 にするFraction add(Fraction a, Fraction b):a + b を約分して返すa/b + c/d = 結果」の形で表示する。入力の分数は約分せずにそのまま表示する)。分子は −1000 以上 1000 以下、分母は 1 以上 1000 以下。結果の分母が 1 でも「2/1」の形で表示する。「実行」で入力を与えて動かす/「採点する」で 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); とします)。この問題の入力では分母は正なので符号の調整は念のための処理ですが、負の分数を扱うときは「分母を正にそろえる」と決めておくと表示が安定します。Item(名前・単価・在庫数)の構造体配列を使って、注文を処理する2つの関数を書け。Item *find_item(Item *items, int n, const char *name):名前が name と一致する商品へのポインタを返す。なければ NULLint order(Item *items, int n, const char *name, int qty):注文を1件処理し、売上金額を返す。商品がなければ「名前: 商品がありません」と表示して 0 を返す。在庫が qty 未満なら「名前: 在庫不足 (残り k 個)」と表示して 0 を返す。在庫が足りれば在庫を減らし、「名前: qty 個 x 単価 円 = 金額 円」と表示して金額を返す「実行」で入力を与えて動かす/「採点する」で 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 はアドレスの比較なので、同じ文字列でも一致しません。