プログラミング

C言語超入門⑨ 成績管理プログラムを作ろう

変数、入力、条件分岐、繰り返し、関数、配列、ポインタ、構造体を組み合わせて小さなプログラムを完成させます。

変数やifは読めるのに、いざ1本のプログラムを書くと手が止まる。ここは、多くの人が「文法を知っている」と「組み合わせられる」の間で悩むところです。

最終回は、最大5人の名前と点数を入力し、合否と平均を出す成績管理を作ります。文字を間違って入力しても固まらないよう、入力の確認も加えます。

コードは少し長めです。ただし、最初から全部を暗記する必要はありません。「入力」「保存」「表示」の3つに分け、分かる部分から追ってみてください。

1. 先に、作るものと入力の約束を決める

基本版の動きは次のとおりです。

  1. 人数を1〜5人で入力する
  2. 1人ずつ名前と0〜100点の点数を入力する
  3. 名前と点数を同じ構造体へ保存する
  4. 60点以上なら合格、それ以外なら不合格と表示する
  5. 全員の平均点を小数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つずつ試してみてください。

第1回から復習 / C言語の記事一覧 / プログラミング基礎の記事一覧

← 記事一覧へ戻る