HNU个人项目互评——中小学数学卷子自动生成程序

发布时间 2023-09-20 09:00:26作者: Narniaaagh

HNU个人项目互评——中小学数学卷子自动生成程序

  • 代码作者:刘蕴哲
  • 评价人:李锦华

一、个人项目需求

1. 用户

小学、初中和高中数学老师。

2. 功能

(1)命令行输入用户名和密码,两者之间用空格隔开(程序预设小学、初中和高中各三个账号,具体见附表),如果用户名和密码都正确,将根据账户类型显示“当前选择为XX出题”,XX为小学、初中和高中三个选项中的一个。否则提示“请输入正确的用户名、密码”,重新输入用户名、密码;

(2)登录后,系统提示“准备生成XX数学题目,请输入生成题目数量(输入-1将退出当前用户,重新登录):”,XX为小学、初中和高中三个选项中的一个,用户输入所需出的卷子的题目数量,系统默认将根据账号类型进行出题。每道题目的操作数在1-5个之间,操作数取值范围为1-100;

(3)题目数量的有效输入范围是“10-30”(含10,30,或-1退出登录),程序根据输入的题目数量生成符合小学、初中和高中难度的题目的卷子(具体要求见附表)。同一个老师的卷子中的题目不能与以前的已生成的卷子中的题目重复(以指定文件夹下存在的文件为准,见5);

(4)在登录状态下,如果用户需要切换类型选项,命令行输入“切换为XX”,XX为小学、初中和高中三个选项中的一个,输入项不符合要求时,程序控制台提示“请输入小学、初中和高中三个选项中的一个”;输入正确后,显示“”系统提示“准备生成XX数学题目,请输入生成题目数量”,用户输入所需出的卷子的题目数量,系统新设置的类型进行出题;

(5)生成的题目将以“年-月-日-时-分-秒.txt”的形式保存,每个账号一个文件夹。每道题目有题号,每题之间空一行;

3. 附表

附表-1:账户、密码

账户类型 账户 密码 备注
小学 张三1 123
张三2 123
张三3 123
初中 李四1 123
李四2 123
李四3 123
高中 王五1 123
王五2 123
王五3 123

附表-2:小学、初中、高中题目难度要求

小学 初中 高中
难度要求 +,-,*./ 平方,开根号 sin,cos,tan
备注 只能有+,-,*./和() 题目中至少有一个平方或开根号的运算符 题目中至少有一个sin,cos或tan的运算符

二、功能测试

1. 登录

用户名密码错误时显示:

登录成功后显示:

2. 生成题目

生成10道题:

3.退出当前用户

输入-1后:

三、代码分析

1. classes.h

(1) 优点

① 注释详细:代码中包含了详细的注释,解释了各个部分的功能和逻辑,有助于其他开发者理解和维护代码。(以下截取一段为例)

// MyLog类是在主程序中可见的单例类,声明了获取实例函数,命令行分析函数,
// 输入获取函数和Level-char数组转化函数
// 作为与命令行广泛交互的类而存在,通过智能指针,会在程序结束时自动销毁
class MyLog {
 public:
  ~MyLog() {}
  // 登录验证函数,掌控用户的登录入口
  // 返回可以代表用户登录成功与否的整型数据
  int VerifyUser();
  // 获取实例函数,经典的懒汉式实现
  // 返回共享型智能指针
  static std::shared_ptr<MyLog> GetInstance();
  // 命令行分析函数,用于登陆后用户的命令分析,
  // 根据不同的输入指令产生不同的响应或是调用特定的函数
  // 输入一个int指针,用于主程序判断执行指令后的程序执行方向
  // 第二个参数式User类型的指针,获取用户信息,改变工作状态需要
  void AnalyseCommand(int* flag, User* user);
  // 输入函数,获取命令行输入并自动去除换行回车
  // 纯功能型函数,因为重用而干脆写成函数
  // 被VerifyUser和AnalyseCommand调用
  void MyInput();
  // 转换函数,因为AnalyseCommand过于冗长而将level到char*的转化
  // 移植到此处,只有AnalyseCommand调用来在终端输出字符串
  std::string TransformLevel(Level input);
  // 获取已注册用户的工作状态:小学,初中,高中
  // 登录时被VerifyUser调用
  Level user_level(int index) { return user_level_[index]; }
  // 获取已存在用户的账户密码,方便比对
  // 登录时被VerifyUser调用
  char* users(int index) { return users_[index]; }

② 采用命名空间:代码中使用了命名空间,有助于避免命名冲突,提高了代码的可维护性。

namespace myproject_classes {}

③ 使用了智能指针:代码中使用了 std::shared_ptr,这有助于避免内存泄漏和管理对象的生命周期。

  // 智能指针,不多赘述
  // 定义成私有是为了单例模式需要客户端无法多次定义
  static std::shared_ptr<MyLog> my_instance_;

④ 使用工厂模式:代码中使用了工厂模式来创建不同类型的用户对象,这有助于扩展代码以支持不同类型的用户。

class UserFactory {}
class TeacherFactory {}

⑤ 模块化:代码将不同的功能模块化,每个功能有自己的函数或类,提高了代码的可读性和可维护性。

class User {}
class MyLog {}
class Teacher {}
class UserFactory {}
class TeacherFactory {}
class MyMath {}
class FileSystem {}

(2) 缺点

① 内存管理问题:User 类中的 account_ 成员使用裸指针 char*,但没有进行内存管理。最好使用 std::string 来管理字符串,以避免内存泄漏和访问越界。

  // 账户密码获取函数
  char* account() { return account_; }

② 缺少异常处理:代码中没有考虑异常处理机制,如果发生错误,程序可能会崩溃而不是 graceful 地处理错误

③ 静态成员函数过多:MyMath 和 FileSystem 类中的所有函数都是静态的,这可能导致代码的可测试性较差。考虑将一些功能分解成更小的非静态成员函数,以提高可测试性和可维护性。

2. classes.cc

(1) 优点

① 文件操作和字符串处理: 代码实现了文件读取和写入的功能,以及字符串处理,这些功能在实际应用中是非常常见和重要的。

// GetBank获取bank文件中的题库
// 由c自带的文件读写实现(某些情况比文件输入流更方便)
// 主要是因为老师说只准用文件输出流,不然文件输入流更方便
// 先声明一个256的文件路径数组
// 然后让account参数合成来的文件路径格式化后赋值给路径数组
// 通过路径数组打开文件,然后一个个字符的读,加到content字符串末尾
// 最后关闭文件,返回content字串,便获得了题库
std::string FileSystem::GetBank(char* account) {
  // 文件路径字符数组
  char dir_path[256] = { NULL };
  // 读取的单个字符
  char c;
  // C++的内容字符串
  std::string content = "";
  snprintf(dir_path, sizeof(dir_path), "./%s/bank.txt", account);
  FILE* bank_file = NULL;
  if (!fopen_s(&bank_file, dir_path, "r")) {
    while ((c = fgetc(bank_file)) != EOF) {
      content += c;
    }
    fclose(bank_file);
  }
  return content;
}
// 创建试卷文件并将题目字符串放入文件的函数
// 首先通过windows的库函数获取本地时间
// 然后通过snprintf将时间格式化输出到字符串tmp中
// 通过格式化的时间串和文件夹名参数生成对应路径
// 通过C++文件输出流配合std::ios::trunc参数创建文件
// 让传入的content字符串,即题目流转到文件中
// 关闭文件即完成了创建
void FileSystem::SetFile(std::string content, std::string name) {
  // windows的系统时变量
  SYSTEMTIME sys;
  // 获取本地时间
  GetLocalTime(&sys);
  // 文件名存储字符串
  char tmp[64] = { NULL };
  snprintf(tmp, sizeof(tmp), "%4d-%02d-%02d-%02d-%02d-%02d",
    sys.wYear, sys.wMonth, sys.wDay,
    sys.wHour, sys.wMinute, sys.wSecond);
  std::string file_name = "./" + name + "/" + std::string(tmp) + ".txt";
  std::ofstream file(file_name, std::ios::out | std::ios::trunc);
  file << content;
  file.close();
}

② 单例模式实现: 代码中使用了单例模式,确保了只有一个实例对象,并提供了一个全局访问点,以保持数据的一致性。(以下截取部分代码)

// MyLog类是在主程序中可见的单例类,声明了获取实例函数,命令行分析函数,
// 输入获取函数和Level-char数组转化函数
// 作为与命令行广泛交互的类而存在,通过智能指针,会在程序结束时自动销毁
class MyLog {
 private:
  // 智能指针,不多赘述
  // 定义成私有是为了单例模式需要客户端无法多次定义
  static std::shared_ptr<MyLog> my_instance_;
}

(2) 缺点

① 代码冗余: 一些函数如Add、Subtract、Multiply、Divide等的代码结构相似,存在大量重复的代码,不利于维护和扩展。可以考虑将这些共通的部分提取出来,减少重复。

② 错误处理不足: 代码中的错误处理相对简单,例如,文件打开失败后没有进行错误处理,可以添加更多的错误检查和异常处理机制,以提高代码的健壮性。

③ 平台依赖性: 代码中使用了一些Windows平台特定的函数,如GetLocalTime,这会导致代码在不同平台上不可移植。如果需要跨平台支持,需要使用跨平台的替代函数或抽象接口。

  // 获取本地时间
  GetLocalTime(&sys);

3. paper_generator.cc

(1) 优点(多数优点此前已经有过介绍,不多赘述)

① 注释详细:代码中包含了详细的注释,解释了各个部分的功能和逻辑,有助于其他开发者理解和维护代码。

② 采用命名空间:代码中使用了命名空间,有助于避免命名冲突,提高了代码的可维护性。

// 以下是需要从classe.h的命名空间中导入的类
using myproject_classes::MyLog;
using myproject_classes::User;
using myproject_classes::UserFactory;
using myproject_classes::TeacherFactory;

③ 使用了智能指针:代码中使用了 std::shared_ptr,这有助于避免内存泄漏和管理对象的生命周期。

④ 使用工厂模式:代码中使用了工厂模式来创建不同类型的用户对象,这有助于扩展代码以支持不同类型的用户。

⑤ 模块化:代码将不同的功能模块化,每个功能有自己的函数或类,提高了代码的可读性和可维护性。

(2) 缺点:

① 内存泄漏潜在问题:代码中使用了 new 来创建 UserFactory 对象,但没有对其进行释放(delete),这可能导致内存泄漏。应该使用智能指针或在适当的时候手动释放内存。

  // 先创建一个工厂,由老师工厂来具体实现
  UserFactory* factory = new TeacherFactory();

② 不支持异常处理:代码没有考虑到异常处理机制,如果在运行时发生异常,程序可能会崩溃而不是 graceful 地处理错误。

③ 单一职责原则:main 函数的功能相对较大,包括用户登录、命令解析和业务逻辑等,这违反了单一职责原则,可以考虑将其拆分成更小的函数或类以提高代码的可维护性。

四、总结

​ 总的来说,刘蕴哲同学的代码是十分优秀的范例,实现了基本功能,拥有优秀的模块化设计,使用了诸如工厂模式、单例模式等设计模式,同时,还使用了智能指针来管理对象的生命周期,整体设计合理、别出心裁,希望日后能在内存泄漏、异常处理等细节方面有所改进。