HNU个人项目评测—中小学数学试卷自动生成程序
目录
一. 简介
二. 项目要求
三. 代码测试
四. 代码分析
五. 总结
正文
一. 简介
本博客是对结对编程好友宋君帆同学的个人项目所撰写的代码的分析与总结。个人项目所使用高级编程语言为C++,编译及运行环境为64位Linux虚拟机(Ubuntu 22.04.3 LTS 64位)。在本篇博客中,我将会根据结对编程好友宋君帆同学所撰写的代码来分析与个人项目要求相应不足的地方,以及其代码的优点,以及缺点。
二. 个人项目要求
个人项目:中小学数学卷子自动生成程序
用户:
小学、初中和高中数学老师。
功能:
1、命令行输入用户名和密码,两者之间用空格隔开(程序预设小学、初中和高中各三个账号,具体见附表),如果用户名和密码都正确,将根据账户类型显示“当前选择为XX出题”,XX为小学、初中和高中三个选项中的一个。否则提示“请输入正确的用户名、密码”,重新输入用户名、密码;
2、登录后,系统提示“准备生成XX数学题目,请输入生成题目数量(输入-1将退出当前用户,重新登录):”,XX为小学、初中和高中三个选项中的一个,用户输入所需出的卷子的题目数量,系统默认将根据账号类型进行出题。每道题目的操作数在1-5之间,操作数的取值范围在1 -100之间;
3、题目数量的有效输入范围是“10-30”(含10,30,或-1退出登录),程序根据输入的题目数量生成符合小学、初中和高中难度的题目的卷子(具体要求见附表)。同一个老师的卷子中的题目不能与以前的已生成的卷子中的题目重复(以指定文件夹下存在的文件为准,见5);
4、在登录状态下,如果用户需要切换类型选项,命令行输入“切换为XX”,XX为小学、初中和高中三个选项中的一个,输入项不符合要求时,程序控制台提示“请输入小学、初中和高中三个选项中的一个”;输入正确后,显示“”系统提示“准备生成XX数学题目,请输入生成题目数量”,用户输入所需出的卷子的题目数量,系统新设置的类型进行出题;
5、生成的题目将以“年-月-日-时-分-秒.txt”的形式保存,每个账号一个文件夹。每道题目有题号,每题之间空一行;
6、个人项目9月17日晚上10点以前提交至创新课程管理系统。提交方式:工程文件打包,压缩包名为“几班+姓名.rar”。迟交2天及以内者扣分,每天扣20%。迟交2天及以上者0分。
附表-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.登录测试

命令行输入账号和密码,以空格隔开。当我输入错误的账号和密码的时候,会提示”请输入正确的用户名和密码“,然后需要我们重新输入账号和密码,当我输入正确的密码后,会提示我”登录成功,当前选择是小学级别的题目生成“,因为张三1的账号类型为小学,所以提示的是小学级别的。
然后测试另外两个不同的账号类型的账号登录。


符合个人项目的要求,能够根据不同的账号类型来提示出不同级别的题目的生成。
2.生成题目以及试卷测试

在本次测试中我所使用的是账号类型为高中的账号进行登录,当我登录成功后,并没有提示”准备生成高中数学题目,请输入生成题目数量(输入-1将退出当前用户,重新登录):“,对不知道程序功能的用户并不友好,因为用户根本不知道下一步应该做什么。根据要求可以知道,这时候应该输入需要生成题目的数目,输入2,可以看到题目命令无效,请再次输入,这是因为有着题目题目数目必须大于等于10,小于等于30的要求。

当我输入满足条件的数后,会在终端显示已经生成的一定数量的随机的题目,然后会询问我是否选择将这些文件保存进入文件中,选择保存到文件中,会在以用户名命名的文件夹中生成一个以当前时间命名的.txt文件。


打开生成的.txt文件,我们可以看到,随机生成的题目已经保存进入文件中,但是还存在着不足的地方,没有在题目前把题号保存进入文件,并且每个题目之间并没有空一行出来。所生成的题目类型也过于单一,仅只有一个三角函数运算。
3.切换难度测试

可以看到,当我们输入”切换至 xx“的时候,如果xx为小学、初中、高中中的任意一个,均可以完成切换,并且能够生成对应难度的试卷。当我们输入账号类型错误的时候,会提醒我们输入错误,需要我们重新输入,当输入正确信息后可以完成切换。并且可以重复出题。
4.退出测试

当我们输入-1的话,能够退出程序。
四. 代码分析
1 //user_auth.h 2 #ifndef USER_AUTH_H 3 #define USER_AUTH_H 4 5 #include <string> 6 #include <unordered_map> 7 8 // Function to initialize the user database with predefined usernames and passwords 9 void initialize_user_db(std::unordered_map<std::string, std::pair<std::string, std::string>>& user_db); 10 11 // Function to authenticate the user using the username and password 12 bool authenticate_user(const std::unordered_map<std::string, std::pair<std::string, std::string>>& user_db, const std::string& username, const std::string& password, std::string& user_type); 13 14 #endif // USER_AUTH_H
这是与用户有关的头文件,只看这个头文件的话,我们根据注释可以得到 initialize_user_db 函数的参数一个无序map容器的指针,这个map容器的索引是string类型,内容是两个string类型由 std::pair 实现的结构体,该函数的作用是用预定义的用户名和密码初始化用户数据库。authenticate_user 函数的作用是使用用户名和密码对用户进行认证,有四个参数,其中一个就是无序map类型的指针,然后另外三个分别是 string 类型指针,username、pawwword、user_type。接下来我们来看这个头文件的实现。
1 //user_auth.cc 2 #include "user_auth.h" 3 #include <iostream> 4 5 void initialize_user_db(std::unordered_map<std::string, std::pair<std::string, std::string>>& user_db) { 6 // Predefined usernames and passwords 7 user_db["张三1"] = {"123", "primary"}; 8 user_db["张三2"] = {"123", "primary"}; 9 user_db["张三3"] = {"123", "primary"}; 10 11 user_db["李四1"] = {"123", "middle"}; 12 user_db["李四2"] = {"123", "middle"}; 13 user_db["李四3"] = {"123", "middle"}; 14 15 user_db["王五1"] = {"123", "high"}; 16 user_db["王五2"] = {"123", "high"}; 17 user_db["王五3"] = {"123", "high"}; 18 } 19 20 bool authenticate_user(const std::unordered_map<std::string, std::pair<std::string, std::string>>& user_db, const std::string& username, const std::string& password, std::string& user_type) { 21 auto it = user_db.find(username); 22 if (it != user_db.end() && it->second.first == password) { 23 user_type = it->second.second; 24 return true; 25 } 26 return false; 27 }
user_auth.cc是对user_authentic.h头文件的实现,可以看到 initialize_user_db 被传入的map类型指针初始化,将预设的账号密码存入map指针所指向的地址中。authenticate_user 函数则是根据传入的 map 指针里的数据对 username 与 password 进行验证,如果在 map 中能够找到索引为 username,内容的第一个string为 password ,就证明登录成功,并将 map 中存储的用户类型返回至user_type,并返回 true ,否则返回false。
1 //question_generator_base.h 2 #ifndef QUESTION_GENERATOR_BASE_H 3 #define QUESTION_GENERATOR_BASE_H 4 5 #include <string> 6 #include <vector> 7 8 // Virtual base class for question generator 9 class QuestionGeneratorBase { 10 public: 11 12 virtual ~QuestionGeneratorBase() {} 13 14 virtual std::vector<std::string> generate_questions(int num_questions) = 0; 15 }; 16 17 #endif // QUESTION_GENERATOR_BASE_H
接下来看 question_generator_base.h 头文件,在这个头文件中定义了一个问题生成器的虚拟基类 QuestionGeneratorBase,在类中定义了两个虚函数,一个是类的析构函数,另一个则是一个返回值为类型为 vector<string> 的问题生成函数 generte_questions,该函数有一个参数,参数为 int 类型,为问题的数目。
1 //primary_question_generator.cc 2 #include "question_generator_base.h" 3 #include <vector> 4 #include <cstdlib> 5 #include <ctime> 6 #include <algorithm> 7 8 class PrimaryQuestionGenerator : public QuestionGeneratorBase { 9 10 public: 11 PrimaryQuestionGenerator() { 12 srand(time(NULL)); 13 } 14 15 std::vector<std::string> generate_questions(int num_questions) override { 16 std::vector<std::string> questions; 17 for (int i = 0; i < num_questions; ++i) { 18 questions.push_back(generate_single_question()); 19 } 20 return questions; 21 } 22 23 private: 24 25 std::string generate_single_question() { 26 std::vector<std::string> operands; 27 std::vector<std::string> operators; 28 int num_operands = rand() % 5 + 1; 29 30 // Generating operands 31 for (int i = 0; i < num_operands; ++i) { 32 int num = rand() % 100 + 1; 33 operands.push_back(std::to_string(num)); 34 } 35 36 // Generating operators 37 char operators_char[] = {'+', '-', '*', '/'}; 38 for (int i = 0; i < num_operands - 1; ++i) { 39 char op = operators_char[rand() % 4]; 40 // Ensuring no division by zero 41 if (op == '/') { 42 operands[i+1] = std::to_string(rand() % 99 + 1); 43 } 44 operators.push_back(std::string(1, op)); 45 } 46 47 // Creating the expression by alternately adding operands and operators 48 std::string question = operands[0]; 49 for (int i = 0; i < operators.size(); ++i) { 50 question += " " + operators[i] + " " + operands[i+1]; 51 } 52 53 // Adding additional operations based on the level 54 55 return question; 56 } 57 58 };
1 //middle_question_generator.cc 2 #include "question_generator_base.h" 3 #include <vector> 4 #include <cstdlib> 5 #include <ctime> 6 #include <algorithm> 7 8 class MiddleQuestionGenerator : public QuestionGeneratorBase { 9 10 public: 11 MiddleQuestionGenerator() { 12 srand(time(NULL)); 13 } 14 15 std::vector<std::string> generate_questions(int num_questions) override { 16 std::vector<std::string> questions; 17 for (int i = 0; i < num_questions; ++i) { 18 questions.push_back(generate_single_question()); 19 } 20 return questions; 21 } 22 23 private: 24 25 std::string generate_single_question() { 26 std::vector<std::string> operands; 27 std::vector<std::string> operators; 28 int num_operands = rand() % 5 + 1; 29 30 // Generating operands 31 for (int i = 0; i < num_operands; ++i) { 32 int num = rand() % 100 + 1; 33 operands.push_back(std::to_string(num)); 34 } 35 36 // Generating operators 37 char operators_char[] = {'+', '-', '*', '/'}; 38 for (int i = 0; i < num_operands - 1; ++i) { 39 char op = operators_char[rand() % 4]; 40 // Ensuring no division by zero 41 if (op == '/') { 42 operands[i+1] = std::to_string(rand() % 99 + 1); 43 } 44 operators.push_back(std::string(1, op)); 45 } 46 47 // Creating the expression by alternately adding operands and operators 48 std::string question = operands[0]; 49 for (int i = 0; i < operators.size(); ++i) { 50 question += " " + operators[i] + " " + operands[i+1]; 51 } 52 53 // Adding additional operations based on the level 54 55 if (rand() % 2 == 0) { 56 question = "(" + question + ") ^ 2"; 57 } else { 58 question = "sqrt(" + question + ")"; 59 } 60 61 return question; 62 } 63 64 };
1 //high_question_generator.cc 2 #include "question_generator_base.h" 3 #include <vector> 4 #include <cstdlib> 5 #include <ctime> 6 #include <algorithm> 7 8 class HighQuestionGenerator : public QuestionGeneratorBase { 9 10 public: 11 HighQuestionGenerator() { 12 srand(time(NULL)); 13 } 14 15 std::vector<std::string> generate_questions(int num_questions) override { 16 std::vector<std::string> questions; 17 for (int i = 0; i < num_questions; ++i) { 18 questions.push_back(generate_single_question()); 19 } 20 return questions; 21 } 22 23 private: 24 25 std::string generate_single_question() { 26 std::vector<std::string> operands; 27 std::vector<std::string> operators; 28 int num_operands = rand() % 5 + 1; 29 30 // Generating operands 31 for (int i = 0; i < num_operands; ++i) { 32 int num = rand() % 100 + 1; 33 operands.push_back(std::to_string(num)); 34 } 35 36 // Generating operators 37 char operators_char[] = {'+', '-', '*', '/'}; 38 for (int i = 0; i < num_operands - 1; ++i) { 39 char op = operators_char[rand() % 4]; 40 // Ensuring no division by zero 41 if (op == '/') { 42 operands[i+1] = std::to_string(rand() % 99 + 1); 43 } 44 operators.push_back(std::string(1, op)); 45 } 46 47 // Creating the expression by alternately adding operands and operators 48 std::string question = operands[0]; 49 for (int i = 0; i < operators.size(); ++i) { 50 question += " " + operators[i] + " " + operands[i+1]; 51 } 52 53 // Adding additional operations based on the level 54 55 std::string trig_functions[] = {"sin", "cos", "tan"}; 56 std::string trig = trig_functions[rand() % 3]; 57 question = trig + "(" + question + ")"; 58 59 return question; 60 } 61 62 };
现在可以看到是有三个 .cc 文件来实现 question_generator_base.h 函数,其中 primary_question_generator.cc 实现小学难度的题目生成,middle_question_generator.cc 实现初中难度的题目生成,high_question_generator.cc 实现高中难度的题目的生成。在这个三个 .cc 文件中分别定义了相应的派生类,共有继承 QuestionGeneratorBase 基类。在派生类中定义了构造函数,用于实现对伪随机的随机数初始化。在三个派生类中又分别声明并实现了以一个问题的生成。通过在从基类中获取的题目生成函数,反复调用一个问题的生成函数,来实现生成试卷。
1 //file_handler.h 2 #ifndef FILE_HANDLER_H 3 #define FILE_HANDLER_H 4 5 #include <string> 6 #include <vector> 7 8 class FileHandler { 9 public: 10 // Method to create a user-specific folder 11 static void CreateUserFolder(const std::string& username); 12 13 // Method to save the questions to a file 14 static void SaveQuestionsToFile(const std::string& username, const std::vector<std::string>& questions); 15 16 // Method to load all the previously saved questions from the user's folder 17 static std::vector<std::string> LoadAllQuestions(const std::string& username); 18 19 // Method to check if the new questions are duplicate 20 static bool CheckForDuplicateQuestions(const std::string& username, const std::vector<std::string>& new_questions); 21 }; 22 23 #endif // FILE_HANDLER_H
现在我们来看 file_handler.h 头文件,在这个头文件中定义了一个 FileHandler 类,并在类中声明了四个共用函数。CreateUserFolder函数的参数是一个静态的 string 类型指针 username,作用是根据这个 username 来创建一个文件夹。SaveQuestionsToFile函数的参数是一个 string 类型的指针 username,还有一个 vector<string>类型的指针 question,作用是将生成的题目保存到文件中去。LoadAllQuestions 函数有一个参数为 string 类型的 username,作用是将以 username 命名的文件夹中的所有以前生成的问题获取出来,并返回 vector<string>。CheckForDuplicateQuestions函数有两个参数,一个是 string 类型的 username ,另一个是 vector<string> 类型的 new_questions,作用是查重,并返回 bool 类型。
1 //file_handler.cc 2 #include "file_handler.h" 3 #include <fstream> 4 #include <iostream> 5 #include <filesystem> 6 #include <chrono> 7 #include <set> 8 9 namespace fs = std::filesystem; 10 11 void FileHandler::CreateUserFolder(const std::string& username) { 12 fs::create_directory(username); 13 } 14 15 void FileHandler::SaveQuestionsToFile(const std::string& username, const std::vector<std::string>& questions) { 16 auto now = std::chrono::system_clock::now(); 17 std::time_t now_c = std::chrono::system_clock::to_time_t(now); 18 struct tm *parts = std::localtime(&now_c); 19 20 char buffer[80]; 21 strftime(buffer, sizeof(buffer), "%Y-%m-%d-%H-%M-%S", parts); 22 23 std::string filename = username + "/" + buffer + ".txt"; 24 std::ofstream file(filename); 25 if (file.is_open()) { 26 for (const auto& question : questions) { 27 file << question << std::endl; 28 } 29 file.close(); 30 } else { 31 std::cerr << "Unable to open file to save questions." << std::endl; 32 } 33 } 34 35 std::vector<std::string> FileHandler::LoadAllQuestions(const std::string& username) { 36 std::vector<std::string> all_questions; 37 std::string path = username; 38 for (const auto& entry : fs::directory_iterator(path)) { 39 std::ifstream file(entry.path()); 40 if (file.is_open()) { 41 std::string line; 42 while (std::getline(file, line)) { 43 if (!line.empty()) { 44 all_questions.push_back(line); 45 } 46 } 47 file.close(); 48 } else { 49 std::cerr << "Unable to open file to load questions." << std::endl; 50 } 51 } 52 return all_questions; 53 } 54 55 bool FileHandler::CheckForDuplicateQuestions(const std::string& username, const std::vector<std::string>& new_questions) { 56 auto all_questions = LoadAllQuestions(username); 57 std::set<std::string> question_set(all_questions.begin(), all_questions.end()); 58 59 for (const auto& question : new_questions) { 60 if (question_set.find(question) != question_set.end()) { 61 return true; 62 } 63 } 64 return false; 65 }
file_handler.cc 是对 file_handler.h 头文件中定义的 类的实现。CreateUserFolder 函数利用 fs::create_directory 来创建与用户名同名的文件夹。SaveQuestionsToFile 函数将用列表保存的生成的文件存放进入以 username 命名的文件夹中,并将保存的 .txt 文件以当前时间命名。LoadAllQuestions 函数则是根据 username 查找文件夹中的所有 .txt 文件,并将其存储进入 vector 中,并返回 vector。CheckForDuplicateQuestions 函数则是调用了 LoadAllQuestions 函数,获取所用题目,并与现在所生成的题目查重,但查重函数存在不足,现在所生成的题目之间没有实现查重。
//main.cc #include "user_auth.h" #include "question_generator_base.h" #include "primary_question_generator.cc" #include "middle_question_generator.cc" #include "high_question_generator.cc" #include "file_handler.h" #include <iostream> #include <string> #include <memory> #include <cstdio> #include <algorithm> void GetUserCredentials(std::string& username, std::string& password) { printf("\n请输入用户名和密码(用空格隔开):"); char uname[50], pword[50]; scanf("%49s %49s", uname, pword); username = uname; password = pword; } void GenerateQuestions(const std::string& user_type, const std::string& username, int num_questions) { // Create appropriate question generator based on user type std::unique_ptr<QuestionGeneratorBase> question_generator; if (user_type == "小学") { question_generator = std::make_unique<PrimaryQuestionGenerator>(); } else if (user_type == "初中") { question_generator = std::make_unique<MiddleQuestionGenerator>(); } else { question_generator = std::make_unique<HighQuestionGenerator>(); } // Generate questions auto questions = question_generator->generate_questions(num_questions); for (const auto& question : questions) { printf("%s\n", question.c_str()); } // Save questions to file char save_option; printf("\n是否将这些问题保存到文件中?(y/n):"); scanf(" %c", &save_option); if (save_option == 'y' || save_option == 'Y') { FileHandler::SaveQuestionsToFile(username, questions); printf("题目已保存到文件中。\n"); } } bool IsNumber(const std::string& s) { return !s.empty() && std::find_if(s.begin(), s.end(), [](unsigned char c) { return !std::isdigit(c); }) == s.end(); } void HandleSwitchCommand(std::string& user_type) { char user_type_char[50]; scanf("%49s", user_type_char); std::string new_user_type(user_type_char); if (new_user_type == "小学" || new_user_type == "初中" || new_user_type == "高中") { user_type = new_user_type; printf("已经成功的切换到 %s。\n", user_type.c_str()); } else { printf("无效的学校类型。请重新输入。\n"); } } int main() { std::unordered_map<std::string, std::pair<std::string, std::string>> user_db; initialize_user_db(user_db); std::string username; std::string password; std::string user_type; while (true) { GetUserCredentials(username, password); if (authenticate_user(user_db, username, password, user_type)) { if (user_type == "primary") user_type = "小学"; else if (user_type == "middle") user_type = "初中"; else user_type = "高中"; printf("登录成功。当前选择是 %s 级别的题目生成。\n", user_type.c_str()); FileHandler::CreateUserFolder(username); break; } else { printf("请输入正确的用户名和密码。\n"); } } while (true) { char input[50]; scanf("%49s", input); std::string input_str(input); if (input_str == "-1") { break; } else if (input_str == "切换至") { HandleSwitchCommand(user_type); } else { if (IsNumber(input_str)) { int num_questions = std::stoi(input_str); if (num_questions >= 10 && num_questions <= 30) { GenerateQuestions(user_type, username, num_questions); } else { printf("无效的命令。请再试一次。\n"); } } else { printf("无效的命令。请再试一次。\n"); } } } return 0; }
最后是主函数文件 main.cc,头文件的使用不和规范。这个文件中实现了主函数 mian 以及一系列的功能函数。主函数 main 首先声明了一个 map 类型变量,并利用 initialize_user_db 函数将预设的用户信息存入变量中。然后声明三个 string 类型变量 username、password、user_type,分别存储用户名、密码、用户类型,使用 GetUserCredentials 函数获取账号和密码,然后使用 authenticate_user 函数判断是否登录成功,登录成功则根据 user_type 进行转义,对 user_type 重定义,然后根据用户名使用 CreateUserFolder 函数创建一个专属文件夹,如果登录失败,则提示“请输入正确的用户名和密码”,然后重乎之前操作。登录成功后,需要根据接下来的输入的语句来执行下一步操作。输入 -1 则退出程序。输入数字,则匹配大小,看是否符合规范,然后如果符合规范,则执行 GenerateQuestions 函数,生成试卷。输入“切换至”,则查看后面的输入否与符合规范,是”小学“、”初中“、”高中“中的一个,则切换至对应难度。输入均不符合规范,则体术无效命令,需要再试一次,然后重复操作,直至程序退出。
五. 总结
宋君帆合理的使用了题目作为虚拟类,以不同难度的题目作为派生类,来分别实现不同难度的题目的生成。灵活运用了 map 、pair、vector 容器来实现数据的存储。能够正确的生成问题,并根据已经生成的问题进行查重,并将其保存进入以当前时间命名的 .txt 文件中去。可以自由的切换问题的难度,可以重复出题等。
但是宋君帆同学生成的小学题目并不符合要求,没有使用(),初中、高中题目符合要求,但生成的题目类型单一,仅仅实现了有且仅有一个根号或平方或三角函数。最终能将生成的题目保存进入文件中去,但是并没有存入相应题目的题号,而且虽然实现了换行处理,但没有实现题目与题目之间的空行。实现了所生成的题目与以前题目的查重,但并没有实现所生成题目相互之间的查重。
宋君帆同学的代码可读性较差。代码运行过程中的文字提示过少,使用者难以知道下一步应该做什么。代码规范还存在部分问题,如在主函数中的头文件的使用。使用预置的用户信息,给后续可能的用户增贴带来不便等。

浙公网安备 33010602011771号