C言語超入門⑨ 成績管理プログラムを作ろう
変数、入力、条件分岐、繰り返し、関数、配列、ポインタ、構造体を組み合わせて小さなプログラムを完成させます。
変数やifは読めるのに、いざ1本のプログラムを書くと手が止まる。ここは、多くの人が「文法を知っている」と「組み合わせられる」の間で悩むところです。
最終回は、最大5人の名前と点数を入力し、合否と平均を出す成績管理を作ります。文字を間違って入力しても固まらないよう、入力の確認も加えます。
コードは少し長めです。ただし、最初から全部を暗記する必要はありません。「入力」「保存」「表示」の3つに分け、分かる部分から追ってみてください。
1. 先に、作るものと入力の約束を決める
基本版の動きは次のとおりです。
- 人数を1〜5人で入力する
- 1人ずつ名前と0〜100点の点数を入力する
- 名前と点数を同じ構造体へ保存する
- 60点以上なら合格、それ以外なら不合格と表示する
- 全員の平均点を小数1桁で表示する
今回は終了するとデータが消えます。ファイルやデータベースへの保存はしていません。まずは架空の練習用データで使ってください。
点数は80のような整数だけを受け付け、80点や80.5は断ります。名前は空白を含められますが、最大49バイトです。UTF-8の日本語では「49文字」という意味ではありません。
2. 入力をscanfだけで繰り返さない理由
scanf("%d", &score)で文字を入力すると、数へ変換できません。さらに、その文字が入力側に残り、同じ読み取りを繰り返して失敗し続けることがあります。
%49sなら文字配列の書き込み量は制限できますが、空白で止まります。また、入力全体が妥当かどうかまでは確認できません。
そこで完成版では、次の2段階にします。
fgetsで1行を文字列として読むstrtolで整数へ変換し、最後まで正しく読めたか確認する
strtolは「文字列をlong型の整数へ変える関数」です。数値の後ろに何が残ったかも調べられます。Microsoftのstrtolの説明で、変換終了位置と範囲エラーの仕組みを確認できます。
3. 基本版の完成コード
C11以降の環境で、全体を1つの.cファイルへ入れてください。学習用環境に「標準入力」欄がある場合は、人数・名前・点数を1行ずつ並べます。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>
#define MAX_STUDENTS 5
typedef struct {
char name[50];
int score;
} Student;
/* 1: 成功、0: 入力終了、-1: 長すぎる */
int readLine(char text[], int capacity) {
if (fgets(text, capacity, stdin) == NULL) {
return 0;
}
char *newline = strchr(text, '\n');
if (newline != NULL) {
*newline = '\0';
} else {
int character = getchar();
if (character != '\n' && character != EOF) {
while ((character = getchar()) != '\n' && character != EOF) {
}
return -1;
}
}
size_t length = strlen(text);
if (length > 0 && text[length - 1] == '\r') {
text[length - 1] = '\0';
}
return 1;
}
int readInteger(const char prompt[], int minimum, int maximum, int *result) {
char text[128];
for (;;) {
printf("%s", prompt);
int status = readLine(text, sizeof(text));
if (status == 0) {
return 0;
}
if (status < 0) {
printf("入力が長すぎます。\n");
continue;
}
char *end;
errno = 0;
long value = strtol(text, &end, 10);
int converted = end != text;
while (isspace((unsigned char)*end)) {
end++;
}
if (!converted || errno == ERANGE || *end != '\0' ||
value < minimum || value > maximum) {
printf("%dから%dの整数を入力してください。\n", minimum, maximum);
continue;
}
*result = (int)value;
return 1;
}
}
int inputStudent(Student *student) {
for (;;) {
printf("名前(最大49バイト): ");
int status = readLine(student->name, sizeof(student->name));
if (status == 0) {
return 0;
}
if (status < 0) {
printf("名前を短くしてください。\n");
continue;
}
if (strspn(student->name, " \t") == strlen(student->name)) {
printf("名前を入力してください。\n");
continue;
}
break;
}
return readInteger("点数(0-100): ", 0, 100, &student->score);
}
void showResults(const Student students[], int count) {
if (count == 0) {
printf("まだデータがありません。\n");
return;
}
int total = 0;
for (int i = 0; i < count; i++) {
printf("%s: %d点 ", students[i].name, students[i].score);
if (students[i].score >= 60) {
printf("合格\n");
} else {
printf("不合格\n");
}
total += students[i].score;
}
printf("平均: %.1f点\n", (double)total / count);
}
int main(void) {
Student students[MAX_STUDENTS] = {0};
int count = 0;
if (!readInteger("人数(1-5): ", 1, MAX_STUDENTS, &count)) {
return 0;
}
for (int i = 0; i < count; i++) {
printf("%d人目\n", i + 1);
if (!inputStudent(&students[i])) {
printf("入力が終了したため、集計せず終了します。\n");
return 0;
}
}
printf("\n--- 結果 ---\n");
showResults(students, count);
return 0;
}
入力の例は次のとおりです。
2
Aoi Sora
80
Ren
59
結果部分はAoi Sora: 80点 合格、Ren: 59点 不合格、平均: 69.5点となります。名前の中の空白も保存されます。
4. まずmainから、全体の流れをつかむ
Student students[MAX_STUDENTS] = {0};で5人分の置き場所を用意します。{0}で初期化していますが、まだ5人を登録したわけではありません。実際に扱う人数はcountで管理します。
#define MAX_STUDENTS 5は、コード中の名前を5へ置き換える指示です。人数の上限を1か所へまとめています。マクロは普通の変数ではないので、行末のセミコロンは付けません。
次にreadIntegerで人数を確認し、forで1人ずつinputStudentへ渡します。
&students[i]は、i番目の学生の場所。inputStudentのstudent->nameやstudent->scoreは、その場所にあるメンバを使う操作です。構造体の回とつなげると、記号の意味が追いやすくなります。
5. 入力関数は3つの失敗を区別する
読み取りに失敗したら終了する
fgetsがNULLを返したら、入力が終わったか読み取りエラーです。readLineは0を返し、上の関数も0を返して、mainまで終了を知らせます。
数値の範囲外とは違い、もう入力がない状態で質問し続けないことが大切です。
長すぎる行は残りを取り除く
fgetsの保存先がいっぱいになると、改行まで読み切れない場合があります。
readLineは次の文字を確認し、続きがあれば改行か入力終端まで取り除いて-1を返します。getcharの結果をintで受けているのは、通常の文字に加え、特別なEOFも区別するためです。
これで、長い名前の後半が次の「点数」へ紛れ込むのを防ぎます。容量ちょうどの行なら、次に読んだ改行を消費して受け付けます。
数字の後ろの文字も確認する
strtol(text, &end, 10)の10は十進数の指定。endには、変換が止まった位置が入ります。
| 入力 | 何が起きるか |
|---|---|
80 |
80へ変換でき、終わりまで読める |
80点 |
80の後ろに文字が残るので拒否 |
abc |
数を1文字も読めないので拒否 |
101 |
変換できるが、点数の範囲外なので拒否 |
| とても大きな整数 | ERANGEなどの確認で拒否 |
errnoはライブラリのエラー状態を確認する仕組みです。変換前に0へ戻し、今回の変換でERANGEになったか調べます。
isspaceで末尾の空白は許可します。文字判定関数へ渡す値の条件を満たすため、unsigned charへ変換しています。ここはC言語特有の注意点です。
入力関数の定義はC11公開委員会草案の7.21.7.2と7.22.1.4でも確認できます。
6. 平均は、整数で割る前に型を変える
(double)total / countは、totalを一時的に小数を扱う型へ変えてから割っています。この変換をキャストと呼びます。
(double)(total / count)では、先に整数の割り算で端数を失うため順番が違います。
showResultsが最初にcount == 0を確認しているのは、0除算と、後の改良で最初の学生を読む危険を防ぐためです。呼び出し側も、countが配列に実際に保存した件数以下であることを守ります。
7. 改良の練習:メニューと最高・最低点を付けよう
次の4つを加えてみましょう。
- 入力と表示をメニューで選べる
- 最高点と最低点を表示する
- 80点以上は「優」、60点以上は「可」、それ以外は「不合格」
- 空白を含む名前を引き続き使える
名前の対応は基本版ですでにできているので、入力関数はそのまま再利用します。
模範解答
基本版のshowResultsとmainだけを、次のコードへ置き換えます。ほかの関数は残してください。
void showResults(const Student students[], int count) {
if (count == 0) {
printf("まだデータがありません。\n");
return;
}
int total = 0;
int highest = students[0].score;
int lowest = students[0].score;
for (int i = 0; i < count; i++) {
int score = students[i].score;
total += score;
if (score > highest) {
highest = score;
}
if (score < lowest) {
lowest = score;
}
printf("%s: %d点 ", students[i].name, score);
if (score >= 80) {
printf("優\n");
} else if (score >= 60) {
printf("可\n");
} else {
printf("不合格\n");
}
}
printf("最高: %d点、最低: %d点\n", highest, lowest);
printf("平均: %.1f点\n", (double)total / count);
}
int main(void) {
Student students[MAX_STUDENTS] = {0};
int count = 0;
for (;;) {
int menu = 0;
if (!readInteger("\n1: 追加 2: 表示 0: 終了\n選択: ",
0, 2, &menu) || menu == 0) {
break;
}
if (menu == 1) {
if (count == MAX_STUDENTS) {
printf("登録できるのは5人までです。\n");
continue;
}
Student candidate = {0};
if (!inputStudent(&candidate)) {
break;
}
students[count] = candidate;
count++;
} else {
showResults(students, count);
}
}
return 0;
}
解説
countは最初0で、入力を最後まで受け取れたときだけ1増やします。candidateという仮の1人へ入力してから配列へ保存するため、途中で入力が終わっても半端なデータを登録しません。
最高・最低は1人目の点数から始め、全員と比べて更新します。80以上を先に調べるので、80点が「可」へ流れることもありません。
メニューは0を選ぶまで繰り返しますが、入力終端でも抜けられます。5人登録後の追加は断り、students[5]へ書き込まないようにしています。
8. 動作確認は、正常な入力以外も試す
| 試すこと | 期待する動き |
|---|---|
| 0・100点 | どちらも受け付ける |
| -1・101点 | 再入力を求める |
abc、80点、空行を点数に入力 |
再入力を求める |
| 空白入りの名前 | 1人の名前として保存 |
| 長すぎる名前 | 拒否し、次の入力へ混ざらない |
| 改良版で登録前に表示 | 「まだデータがありません」 |
| 改良版で6人目を追加 | 上限を案内 |
| 入力終端 | 質問を繰り返さず終了 |
「正しい値なら動く」の次に、「間違った値でも壊れない」を確認するのが、今回の大事な一歩です。手元の環境で表の項目を1つずつ試してみてください。