プログラミング

C++超入門⑩ 点数管理プログラムを完成させよう

変数、if文、while文、for文、関数、vector、構造体、参照を組み合わせ、複数人の点数を管理するプログラムを作ります。

変数、if、for、vector……1つずつは分かってきたけれど、全部をどうつなげればよいのでしょうか。最終回では、名前と点数を入力する小さな管理プログラムにまとめます。

長いコードを一気に理解しようとすると、さすがに疲れます。まず何を保存するか → どう入力するか → どう表示するかの順に分けてみましょう。

今回はC++17以降を使います。入力間違いで無限ループにならないよう、文字や範囲外の値を確認する処理も入れます。

1. 完成形を先に決める

基本版には次の機能を付けます。

  • 人数を1〜100人で指定する
  • 空白を含む名前と、0〜100の整数の点数を入力する
  • 1人分をStudent構造体、全員分をvectorで保存する
  • 60点以上を合格と表示する
  • 数字以外や範囲外は入力し直せる
  • 入力が終了したら、無限に質問せず終了する

これは学習用です。終了するとデータは消え、ファイルやデータベースには保存しません。練習には架空の名前と点数を使ってください。

2. データの形は「1人分」と「全員分」に分ける

struct Student {
    std::string name;
    int score = 0;
};

std::vector<Student> students;

Studentが1人分、studentsが全員分です。vectorは初め空で、入力が終わった人からpush_backで追加します。

入力途中のStudentをすぐ登録しないのがポイントです。名前だけ入って点数がない状態で終わっても、未完成のデータを集計しないようにします。

3. cinの失敗状態を繰り返さないために

std::cin >> scoreは、簡単な入力には便利です。ただし、文字で失敗した後に何も整理せず繰り返すと、失敗状態のまま処理が進み続けることがあります。

cin.clear()で状態を戻し、ignoreで残った入力を取り除く方法もありますが、この完成版では入力を次のように統一します。

  1. std::getlineで1行を文字列として受け取る
  2. std::istringstreamで、その文字列から整数を読む
  3. 余計な文字が残っていないかと、範囲を確認する

istringstreamは、文字列を入力元として扱えるストリームです。キーボードから直接読む代わりに、受け取った1行を検査するために使います。

数字と名前を同じgetline方式へそろえると、「数字の後に残った改行で、名前入力が飛ぶ」問題も避けやすくなります。getlineの仕様と数値読み取りの仕様を確認して使っています。

4. 基本版の完成コード

全体を1つのscores.cppへ保存してください。GCC系の環境ではg++ -std=c++17 -Wall -Wextra scores.cpp -o scoresでコンパイルできます。

#include <iostream>
#include <sstream>
#include <string>
#include <vector>

constexpr int MAX_STUDENTS = 100;
constexpr int PASS_SCORE = 60;

struct Student {
    std::string name;
    int score = 0;
};

bool readInteger(const std::string& prompt,
                 int minimum, int maximum, int& result) {
    for (;;) {
        std::cout << prompt;

        std::string line;
        if (!std::getline(std::cin, line)) {
            return false;
        }

        std::istringstream input(line);
        int value = 0;
        char extra = '\0';

        if ((input >> value) && !(input >> extra) &&
            value >= minimum && value <= maximum) {
            result = value;
            return true;
        }

        std::cout << minimum << "から" << maximum
                  << "の整数を入力してください。\n";
    }
}

bool inputStudent(Student& student) {
    for (;;) {
        std::cout << "名前(120バイトまで): ";
        if (!std::getline(std::cin, student.name)) {
            return false;
        }

        if (student.name.find_first_not_of(" \t\r") != std::string::npos &&
            student.name.size() <= 120) {
            break;
        }
        std::cout << "空白だけでない、短い名前を入力してください。\n";
    }

    return readInteger("点数(0-100): ", 0, 100, student.score);
}

void showResult(const Student& student) {
    std::cout << student.name << ": " << student.score << "点 ";
    if (student.score >= PASS_SCORE) {
        std::cout << "合格\n";
    } else {
        std::cout << "もう一度復習しましょう\n";
    }
}

int main() {
    int count = 0;
    if (!readInteger("人数(1-100): ", 1, MAX_STUDENTS, count)) {
        return 0;
    }

    std::vector<Student> students;

    for (int i = 0; i < count; i++) {
        Student student{};
        std::cout << i + 1 << "人目\n";

        if (!inputStudent(student)) {
            std::cout << "入力が終了したため、集計せず終了します。\n";
            return 0;
        }

        students.push_back(student);
    }

    std::cout << "\n結果\n";
    for (const Student& student : students) {
        showResult(student);
    }
    return 0;
}

標準入力欄へまとめて入れる場合の例です。

2
Aoi Sora
80
Ren
59

結果部分は次のとおりです。

結果
Aoi Sora: 80点 合格
Ren: 59点 もう一度復習しましょう

ここまで動いたら、いきなり改造せず、関数ごとの役割を読み解きます。

5. 入力関数を3段階で読む

getlineの成功を確認する

if (!std::getline(std::cin, line))は、1行を読めなかった場合です。入力終端や読み取りエラーならfalseを返し、呼び出し元も終了します。

再入力してほしい間違いと、もう入力が来ない状態を分けています。後者まで繰り返すと止まらない原因になります。

整数が読めたか、後ろが空かを調べる

input >> valueが成功したら、続いてinput >> extraを試します。

extraは1文字用の変数です。通常の読み取りでは空白を飛ばすので、余計な文字が見つかれば成功し、!(input >> extra)がfalseになります。つまり入力を拒否します。

1行の入力 結果
80 受け付ける
80 前後の空白を許して受け付ける
80点 余計な文字があるので拒否
80.5 小数部分が残るので拒否
abc 整数を読めないので拒否
とても大きな整数 intとして読めず拒否
101 読めるが点数の範囲外なので拒否

入力後に毎回新しいistringstreamを作るため、前回の失敗状態を引きずりません。

成功したときだけ結果を書き込む

int& resultは、呼び出し元の変数への参照です。条件をすべて通った後でresult = valueとするので、不正な入力で結果を上書きしません。

readIntegerは成功ならtrue、入力終了ならfalseを返します。mainはその戻り値を見て、続けるか終わるかを選んでいます。

6. 構造体・vector・const参照のつながり

students.push_back(student)は、入力を終えた1人分をvector末尾へ保存する操作です。この例ではstudentの値をコピーして保存します。

その後、ループ内の一時変数studentが消えても、vectorへ保存した値は残ります。ローカル変数のアドレスだけを保存しているわけではありません。

表示側のfor (const Student& student : students)は、各要素をコピーせず読む形です。showResult(const Student& student)にもconst参照で渡し、表示中にデータを変更しない約束をしています。

「同じStudentを、保存時は値として持ち、表示時は参照で読む」という使い分けです。

7. 改造の練習:一覧を繰り返し使えるようにする

次の5つを入れた版へ進みましょう。

  1. 合計点と平均点を表示する
  2. 最高点の人を表示する
  3. 合格点を定数へまとめる
  4. 名前に空白を含められるようにする
  5. 追加・表示・終了をメニューで選べるようにする

3と4は基本版で組み込んであるので、その部分は再利用します。最高点が同点なら、今回は先に登録した人を表示するルールです。

模範解答

基本版のmainを取り除き、代わりに次のshowResultsとmainを追加してください。それ以外の構造体・入力関数・定数・showResultは残します。

void showResults(const std::vector<Student>& students) {
    if (students.empty()) {
        std::cout << "まだデータがありません。\n";
        return;
    }

    int total = 0;
    const Student* highest = &students[0];

    for (const Student& student : students) {
        total += student.score;

        if (student.score > highest->score) {
            highest = &student;
        }

        std::cout << student.name << ": " << student.score << "点 ";
        if (student.score >= 80) {
            std::cout << "優\n";
        } else if (student.score >= PASS_SCORE) {
            std::cout << "可\n";
        } else {
            std::cout << "不合格\n";
        }
    }

    const double average =
        static_cast<double>(total) / students.size();

    std::cout << "合計: " << total << "点\n";
    std::cout << "平均: " << average << "点\n";
    std::cout << "最高点: " << highest->name
              << "さん " << highest->score << "点\n";
}

int main() {
    std::vector<Student> students;

    for (;;) {
        int menu = 0;
        if (!readInteger("\n1: 追加  2: 表示  0: 終了\n選択: ",
                         0, 2, menu) || menu == 0) {
            break;
        }

        if (menu == 1) {
            if (students.size() >= MAX_STUDENTS) {
                std::cout << "登録できるのは100人までです。\n";
                continue;
            }

            Student student{};
            if (!inputStudent(student)) {
                break;
            }
            students.push_back(student);
        } else {
            showResults(students);
        }
    }
    return 0;
}

解説:合計と平均

totalを0から始め、各学生のscoreを加えます。平均は整数で割る前にstatic_cast<double>で小数を扱う型へ変えます。

空のvectorは先に断るので0除算しません。人数を100、点数を100までと決めたため、合計の最大は10000です。この設計ではintの範囲に収まります。上限を大きく変えるなら、型と最大合計を改めて確認します。

解説:最高点の人をポインタで覚える

highestは、現在の最高点の学生を指すポインタです。最初はstudentsの先頭を指し、より高い点数を見つけたときだけ付け替えます。

highest->scoreは(*highest).scoreと同じ意味です。点数だけでなく、その人のnameも最後に表示できる点が便利です。

この関数は参照中にvectorへ追加していません。 追加により再確保が起きると要素のポインタが無効になるため、最高点を探しながらpush_backするような変更は避けます。vector変更時の無効化の仕様にも注意が必要です。

解説:メニューで追加を繰り返す

for (;;)は継続条件を省いたループです。0を選んだときや入力が終わったときにbreakで抜けます。

登録前に「表示」を選んでも、empty()の確認で案内だけ出ます。100人の状態で「追加」を選んだ場合も、上限を確認してから戻るので、際限なく増える設計にはしていません。

8. 直したら、この順番で確認する

操作・入力 期待する動き
点数0・59・60・79・80・100 境目どおりの評価
点数-1・101 再入力
abc、80点、空行 再入力
空白入りの名前 名前全体を保存
空白だけの名前 再入力
登録前に表示 データなしの案内
同点の最高点を2人登録 先に登録した人を表示
入力終端 失敗状態で回り続けず終了

名前の120という制限はバイト単位の学習用ルールで、日本語120文字を意味しません。getline自体は先に行全体を読むため、巨大な外部入力に対する厳密なメモリ制限を備えたサービスではありません。

ここまでできたら

文法を全部使うことより、「正常なデータだけを保存する」「表示中は変更しない」「失敗なら安全に抜ける」と役割を分けられたことが大きな一歩です。

次は保存機能や並べ替えなどが候補ですが、一度に増やさず、今あるコードの値の流れを説明できる状態にしてから進めましょう。

前の記事 / 第1回から復習 / C++シリーズ一覧

← 記事一覧へ戻る