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で残った入力を取り除く方法もありますが、この完成版では入力を次のように統一します。
std::getlineで1行を文字列として受け取るstd::istringstreamで、その文字列から整数を読む- 余計な文字が残っていないかと、範囲を確認する
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つを入れた版へ進みましょう。
- 合計点と平均点を表示する
- 最高点の人を表示する
- 合格点を定数へまとめる
- 名前に空白を含められるようにする
- 追加・表示・終了をメニューで選べるようにする
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自体は先に行全体を読むため、巨大な外部入力に対する厳密なメモリ制限を備えたサービスではありません。
ここまでできたら
文法を全部使うことより、「正常なデータだけを保存する」「表示中は変更しない」「失敗なら安全に抜ける」と役割を分けられたことが大きな一歩です。
次は保存機能や並べ替えなどが候補ですが、一度に増やさず、今あるコードの値の流れを説明できる状態にしてから進めましょう。