Skip to content

2.7 回顾序言中的成绩程序 ​

经过一段时间的学习,不妨回望一下 序言中的学生成绩程序,读取学生成绩、检查成绩是否合法、筛选不及格学生、输出结果,看看我们有什么长进。

明确职责 ​

  • Score 保证成绩始终在 0 到 100 之间,是不变式;
  • Student 以结构体组合学号、姓名和成绩。

这里没有需要运行时替换的学生类型,因此不需要虚函数或继承树。Student 以结构体存在很合适。

成绩类 ​

cpp
#include <optional>

class Score {
  public:
    [[nodiscard]] static std::optional<Score> from(int value) {
        if (value < 0 || value > 100)
            return std::nullopt;
        return Score{value};
    }

    [[nodiscard]] int value() const noexcept { return value_; }

    [[nodiscard]] bool passed(int threshold = 60) const noexcept {
        return value_ >= threshold;
    }

  private:
    explicit Score(int value) : value_{value} {}
    int value_;
};

Score 类的构造函数是 private 的,并加上了 explicit 防止隐式转换。这意味着创建新的分数只能通过静态的 Score::from 方法实现。

cpp
auto foo = Score::from(80);

Score bar{80};
// 'Score::Score(int)' is private within this context

这是因为分数越界是一个可能发生的失败。这样做不允许调用者构造非法的 Score,否则一旦调用者拿到 Score,后续代码就可以依赖它的不变式。而 Score::from 返回 std::optional<Score>,在遇到非法值时返回 std::nullopt,避免构造非法值。

Score 只保存一个整数,没有管理动态内存或者其他资源,不需要手写析构、复制或移动,编译器会自动管理好行为 —— Rule of Zero。

学生信息结构体 ​

cpp
struct Student {
    int id{};
    std::string name;
    Score score;
};

Student 使用值语义。std::string 会自己管理文本资源,因此 Student 不需要手写析构、复制或移动操作 —— Rule of Zero。

读入学生信息的函数 ​

cpp
std::optional<Student> read_student(std::istream &input) {
    int id{};
    std::string name;
    int raw_score{};

    if (!(input >> id >> name >> raw_score))
        return std::nullopt;

    const auto score = Score::from(raw_score);
    if (!score)
        return std::nullopt;

    return Student{id, std::move(name), *score};
}

read_student 返回 std::optional<Student>。返回值优先,成功时返回一个完整的 Student,失败时返回空值,十分清晰。在返回时,编译器会自动进行复制消除等优化,无需手动 std::move。

主函数 ​

cpp
int main() {
    int count{};
    std::cout << "Number of students: ";
    if (!(std::cin >> count) || count < 0) {
        std::cerr << "Invalid student count\n";
        return 1;
    }

    std::vector<Student> students;
    students.reserve(static_cast<std::size_t>(count));

    std::cout << "Enter id, name and score for each student:\n";
    for (int index{}; index < count; ++index) {
        auto student = read_student(std::cin);
        if (!student) {
            std::cerr << "Invalid student data\n";
            return 1;
        }
        students.push_back(std::move(*student));
    }

    auto failed_students =
        students | std::views::filter([](const Student &student) {
            return !student.score.passed();
        });

    std::cout << "Students who failed:\n";
    for (const Student &student : failed_students) {
        std::cout << student.id << ' ' << student.name << ' '
                  << student.score.value() << '\n';
    }
}

在输入人数后,使用 students.reserve 预留空间。在输入循环中,students.push_back(std::move(*student)) 中使用了 std::move 以使用移动语义。

std::views::filter 创建一个借用 students 的视图。视图只在后面的 for 中使用,此时 students 仍然存活,因此生命周期是安全的。

小结 ​

完整代码
cpp
#include <cstddef>
#include <iostream>
#include <optional>
#include <ranges>
#include <string>
#include <utility>
#include <vector>

class Score {
  public:
    [[nodiscard]] static std::optional<Score> from(int value) {
        if (value < 0 || value > 100)
            return std::nullopt;
        return Score{value};
    }

    [[nodiscard]] int value() const noexcept { return value_; }

    [[nodiscard]] bool passed(int threshold = 60) const noexcept {
        return value_ >= threshold;
    }

  private:
    explicit Score(int value) : value_{value} {}
    int value_;
};

struct Student {
    int id{};
    std::string name;
    Score score;
};

std::optional<Student> read_student(std::istream &input) {
    int id{};
    std::string name;
    int raw_score{};

    if (!(input >> id >> name >> raw_score))
        return std::nullopt;

    const auto score = Score::from(raw_score);
    if (!score)
        return std::nullopt;

    return Student{id, std::move(name), *score};
}

int main() {
    int count{};
    std::cout << "Number of students: ";
    if (!(std::cin >> count) || count < 0) {
        std::cerr << "Invalid student count\n";
        return 1;
    }

    std::vector<Student> students;
    students.reserve(static_cast<std::size_t>(count));

    std::cout << "Enter id, name and score for each student:\n";
    for (int index{}; index < count; ++index) {
        auto student = read_student(std::cin);
        if (!student) {
            std::cerr << "Invalid student data\n";
            return 1;
        }
        students.push_back(std::move(*student));
    }

    auto failed_students =
        students | std::views::filter([](const Student &student) {
            return !student.score.passed();
        });

    std::cout << "Students who failed:\n";
    for (const Student &student : failed_students) {
        std::cout << student.id << ' ' << student.name << ' '
                  << student.score.value() << '\n';
    }
}
  • 先按职责拆分问题,再决定哪些部分需要成为类
  • Score 用类维护不变式,Student 用结构体表达数据
  • 标准库容器和 optional 让资源管理遵循 Rule of Zero
  • optional、范围视图和返回值让控制流直接体现在类型与代码中