一、简介
本博客是对结对编程队友ly的个人项目代码的分析和总结
- 项目内容:中小学数学卷子自动生成程序
- 实现语言:C++
- 代码结构:按功能封装代码,分登陆、显示菜单、随机生成三大类功能来完成该程序的构建。
二、代码运行结果
1.登陆
提示用户输入账号名和密码。

登陆失败,提示重新输入。

登陆成功,进入菜单页面。

输入“3”可以清屏,输入“-1"则退出登陆。
2.菜单
菜单栏有四项选择,主要功能模块是【1】和【2】。
【1】出题

提示输入出题数量,输入后即可生成txt文件,打开对应账号的文件夹即可看到用当前试卷的生成时间为文件名的txt文件。

输入“-1"返回上一级菜单,输入其他数字则提示输入错误。

【2】切换试题难度


当进入该菜单功能时,输入“切换为高中”或“初中”,都可以切换当前题目难度。任意输入时,会提示错误并请求重新输入。

切换题目难度后,提示输入出题数量,输入范围内数字即可生成试卷txt文件。
输入“-1”返回上一级菜单。
3.查看生成文件
-
小学

-
初中

-
高中

随机题目均符合项目需求。
三、功能代码分析
三大模块均由为.h和.cpp两部分组成,头文件中声明了该类模块使用的变量即类函数,cpp文件中实现了对应类函数,提供接口供其他对象调用。
1.登陆模块
文件流输入并比对user.txt中的账户密码。
/* login.h Created by LY on 2022/9/7 */
#ifndef LOGIN_H
#define LOGIN_H
class login {
public:
std::string type;
std::string id;
std::string pwd;
int state;
login();//构造函数
std::string get_type();
std::string get_id();
std::string get_pwd();
};
#endif
/* login.cpp Created by LY on 2022/9/7 */
#include <iostream>
#include <fstream>
#include "login.h"
using namespace std;
login::login() {
string temp_type, temp_id, temp_pwd, in_id, in_pwd;
state=0;
cout << "\n请输入账号名 " << endl;
cin >> in_id;
cout << "\n请输入密码 " << endl;
cin >> in_pwd;
fstream io_user;
io_user.open("data\\user.txt",ios::out | ios::in);
if(io_user) {
while(io_user >> temp_type >> temp_id >> temp_pwd) {
if(temp_id == in_id && temp_pwd == in_pwd) {
this->type=temp_type;
this->id = in_id;
this->pwd = in_pwd;
state=1;
cout<<"\n==================== 登陆成功 ====================\n"<<endl;
if(temp_type == "1")
cout<<"当前选择为【小学】出题" <<endl;
else if(temp_type == "2")
cout<<"当前选择为【初中】出题" <<endl;
else if(temp_type == "3")
cout<<"当前选择为【高中】出题" <<endl;
break;
}
}
} else
cout<<"sys_error: file open error!"<<endl;
io_user.close();
}
string login::get_type() {
return this->type;
}
string login::get_id() {
return this->id;
}
string login::get_pwd() {
return this->pwd;
}
2.菜单模块
通过show_menu()登陆菜单页面,该函数调用了login的构造函数从而关联了登陆模块。
get_test(login *user):通过传入login用户指针,调用随机生成试卷模块而获得txt试卷文件。
handoff(login *user):切换出题难度,传参调用get_test(login *user)获取试卷。
/* menu.h Created by LY on 2022/9/7 */
#ifndef MENU_H
#define MENU_H
#include "login.h"
#include "generate.h"
class menu {
public:
menu();
void show_menu();//登陆菜单页面
void get_test(login *user);//进入个人页面
void handoff(login *user);//切换试题类型
};
#endif
/* menu.h Created by LY on 2022/9/7 */
#include <iostream>
#include "menu.h"
using namespace std;
menu::menu() {
}
void menu::show_menu() {
while(true) {
cout<<"\n======= 欢迎使用中小学数学卷子自动生成程序 ======="<<endl;
cout<<"\n=============== 请使用账号密码登陆 ==============="<<endl;
login *user=new login();
if(user->state != 1) {
cout<<"登陆失败,请检查账号密码是否正确!"<<endl;
//system("cls");
} else { //登陆成功
while(true) {
cout<<"\n请选择对应标号完成操作"<<endl;
cout<<"【1】出题"<<endl;
cout<<"【2】切换试题难度(小学 初中 高中)"<<endl;
cout<<"【3】清屏"<<endl;
cout<<"【-1】退出登陆"<<endl;
int ins;
cin>>ins;
if(ins==1) {
this->get_test(user);
} else if(ins==2) {
this->handoff(user);
} else if(ins==3) {
system("cls");
} else if(ins==-1) {
cout<<"\n============== 退出登录,感谢使用! ==============\n\n"<<endl;
break;
} else {
cout<<"指令错误,请重新输入"<<endl;
}
}
}
}
}
void menu::get_test(login *user) {
generate *g=new generate(user->get_id());
while(true) {
cout<<"\n请输入出题数量(10-30道) [输入-1返回上一级]"<<endl;
int amount;
cin>>amount;
if(amount==-1) {
return;
} else if(amount>=10&&amount<=30) {
g->generate_test(user->get_id(),user->get_type(),amount);
} else {
cout<<"请输入10-30之间的数字(包括10和30)\n"<<endl;
}
}
}
void menu::handoff(login *user) {//切换页面
while(true) {
cout<<"\n请选择输入:切换为小学/初中/高中 [输入-1返回上一级]"<<endl;
string str;
cin>>str;
if(str=="切换为小学"||str=="小学") {
user->type="1";
cout<<"\n进入【小学】出题模式"<<endl;
this->get_test(user);
} else if(str=="切换为初中"||str=="初中") {
user->type="2";
cout<<"\n进入【初中】出题模式"<<endl;
this->get_test(user);
} else if(str=="切换为高中"||str=="高中") {
user->type="3";
cout<<"\n进入【高中】出题模式"<<endl;
this->get_test(user);
} else if(str=="-1") {
return;
} else {
cout<<"超出出题范围:请选择输入小学/初中/高中"<<endl;
}
}
}
3.随机生成试卷模块
实现思路:
- 初始化set,把该用户文件夹目录下的所有题目放置于set中以便后面的查重工作。
- 获取路径:生成放了所有题目的txt文件路径以及用当前文件生成时间作文件名的路径。若无该路径则创建。
- 生成算术题:【重点】定义了多个变量来限制随机生成的条件,以此代替了一些循环,减少迭代次数。
- 查重算术题并放入txt中:使用set实现查重,使用文件流写入txt中。
点击查看代码
/* generate.h Created by LY on 2022/9/7 */
#ifndef GENERATE_H
#define GENERATE_H
#include <set>
using namespace std;
class generate {
public:
std::set<std::string> myset;
generate(std::string id);
std::string generate_time();//生成当前时间
std::string generate_path(std::string id,int type);//生成所需路径
std::string generate_sign(std::string grade); //随机生成带符号数字
std::string generate_expression(std::string grade);//随机生成表达式
void generate_test(std::string id,std::string grade,int amount);//生成卷子 并查重
};
#endif
/* generate.cpp Created by LY on 2022/9/7 */
#include <iostream>
#include <fstream>
#include <sys/types.h> //opendir()
#include <dirent.h>
#include <time.h> //获取当前日期时间
#include <stdio.h> //sprintf_s
#include <stdlib.h> //rand()
#include <string>
#include "generate.h"
using namespace std;
/*
实现思路:
初始化set
获取路径
生成算术题
查重算术题并放入txt中
*/
//登陆之后 初始化set
generate::generate(string id) {
srand((unsigned)time(NULL));//随机种子要放在for循环外才能有随机式子产生
string path=this->generate_path(id,0);//获取all.txt路径
this->myset= {};
fstream init;
init.open(path,ios::in|ios::app);//读并追加
string temp;
while(init>>temp) {
this->myset.insert(temp);//插入set中
}
init.close();
}
string generate::generate_time() { //获取当前时间
time_t timep;
struct tm *p;
char name[256]= {0};
time(&timep);//获取从1970至今过了多少秒,存入time_t类型的timep
p=localtime(&timep);//用localtime将秒数转化为struct tm结构体
sprintf(name,"%d-%02d-%02d-%02d-%02d-%02d.txt",
p->tm_year+1900,p->tm_mon+1,p->tm_mday,p->tm_hour,p->tm_min,p->tm_sec);
string time=name;
return time;
}
string generate::generate_path(string id,int type) { //获取路径 0 all.txt 1 以时间命名.txt
string temp="Math_test\\"+id+"\\";
//路径不存在则创建
if( opendir(temp.c_str()) == NULL) { // opendir(const char*) .c_str()将string直接转换成const char *类型
char s[100];
sprintf(s,"%s %s","mkdir",temp.c_str());//
system(s);
}
if(type==0) { //创建的是all.txt
temp+="all.txt";
return temp;
} else if(type==1) {
string time=this->generate_time();//获取当前时间,创建 以时间为名.txt
temp+=time;
return temp;
}
}
string generate::generate_sign(string grade) {
string sign2[]= {"^2","√"};
string sign3[]= {"sin","cos","tan"};
int num=rand()%100+1;
string temp="";
if(grade=="2") {
if(rand()%2==0) temp=to_string(num)+sign2[0];
else temp=temp=sign2[1]+to_string(num);
return temp;
}
if(grade=="3") {
if(rand()%3==0) temp=sign3[0]+to_string(num);
else if(rand()%3==1) temp=sign3[1]+to_string(num);
else {
if(num%90==0)
num+=rand()%88+1;
temp=sign3[2]+to_string(num);
}
return temp;
}
}
string generate::generate_expression(string grade) {
int OpNum;//记录操作数个数的变化
int par=0;//括号个数
int par_l=0;//左括号多余的个数
int gap=0;//左右括号之间的跨度(只算操作数)
int flag1=0;//平方根号个数
int flag2=0;//三角函数个数
int sym1=0;//是否有平方或根号
int sym2=0;//是否有三角函数
string str="";
string sign1[]= {"+","-","*","/"};
string sign2[] = {"^2","√"};
string sign3[]= {"sin","cos","tan"};
if(grade=="1") { //小学
OpNum=rand()%4+2;//操作数>=2 2 3 4 5
par=rand()%4;//括号个数 随机 0 1 2 3
while(true) {
if(rand()%2==1&&par>0&&OpNum>=2) { // 1/2的概率且操作数≥2时在数前加入左括号
str+="(";//加入左括号
par--;//所剩括号--
par_l++;//左括号多余++
gap=0;//当前括号左右跨度 初始化
}
if(OpNum>0) { //加入操作数
int num=rand()%100+1;
str+=to_string(num);
gap++;
OpNum--;
}
if(rand()%2==1&&par_l>0&&gap>=2) { // 1/2的几率且有左括号未配对且跨度≥2时加入有括号
str+=")";
par_l--;//左括号多余--
}
if(OpNum<=0&&par_l<=0) { //用完所有操作数且无左括号多余即可
break;
}
if(OpNum>0) {
str+=sign1[rand()%4];
}
}
}
if(grade=="2") {
OpNum=rand()%5+1;//操作数>=1 1 2 3 4 5
par=rand()%4;//括号个数 随机 0 1 2 3
flag1=rand()%5+1;//平方开方数量 1 2 3 4 5
while(true) {
if(rand()%2==1&&par>0&&OpNum>=2) { // 1/2的概率且操作数≥2时在数前加入左括号
str+="(";//加入左括号
par--;//所剩括号--
par_l++;//左括号多余++
gap=0;//当前括号左右跨度 初始化
}
if(OpNum==1&&sym1==0&&flag1>0) { //特殊情况 实在没有平方开方则在最后面加上
str+=this->generate_sign("2");//加入带根号开方的操作数
gap++;
OpNum--;
flag1--;//平方根号数量--
sym1=1;//平方根号标志位置1
}
if(OpNum>0) { // 1/2概率加入操作数或带根号平方的操作数
if (rand()%2==1 && flag1>0 ) {
str+=this->generate_sign("2");//加入带根号开方的操作数
gap++;
OpNum--;
flag1--;//平方根号数量--
sym1=1;//平方根号标志位置1
} else {
int num=rand()%100+1;
str+=to_string(num);//加入普通操作数
gap++;
OpNum--;
}
}
if( rand()%2==1 && par_l>0 && gap>=2 ) { // 1/2的几率且有左括号未配对且跨度≥2时加入有括号 前面不能是根号
str+=")";
par_l--;//左括号多余--
}
if(OpNum<=0 && par_l<=0 && sym1==1) { //用完所有操作数且无左括号多余且至少有一个平方开方即可
break;
}
if(OpNum>0&&str!="") {
str+=sign1[rand()%4];
}
}
}
if(grade=="3") {
OpNum=rand()%5+1;//操作数>=1 1 2 3 4 5
par=rand()%4;//括号个数 随机 0 1 2 3
flag1=rand()%5+1;//三角函数数量 1 2 3 4 5
flag2=rand()%5+1;//三角函数数量 1 2 3 4 5
while(true) {
if(rand()%2==1&&par>0&&OpNum>=2) { // 1/2的概率且操作数≥2时在数前加入左括号
str+="(";//加入左括号
par--;//所剩括号--
par_l++;//左括号多余++
gap=0;//当前括号左右跨度 初始化
}
if(OpNum==1&&sym2==0&&flag2>0) { //特殊情况 实在没有三角函数则在最后面加上
str+=this->generate_sign("3");//加入带三角函数的操作数
gap++;
OpNum--;
flag2--;//三角函数数量--
sym2=1;//三角函数标志位置1
}
if(OpNum>0) { // 1/2概率加入操作数或带三角函数的操作数
if (rand()%3==0 && flag2>0 ) {
str+=this->generate_sign("3");//加入带三角函数的操作数
gap++;
OpNum--;
flag2--;//三角函数数量--
sym2=1;//平三角函数标志位置1
} else if(rand()%3==1) {
str+=this->generate_sign("2");//加入带根号开方的操作数
gap++;
OpNum--;
flag1--;//平方根号数量--
sym1=1;//平方根号标志位置1
} else {
int num=rand()%100+1;
str+=to_string(num);//加入普通操作数
gap++;
OpNum--;
}
}
if( rand()%2==1 && par_l>0 && gap>=2 ) { // 1/2的几率且有左括号未配对且跨度≥2时加入有括号 前面不能是根号
str+=")";
par_l--;//左括号多余--
}
if(OpNum<=0 && par_l<=0 && sym2==1) { //用完所有操作数且无左括号多余且至少有一个平方开方即可
break;
}
if(OpNum>0&&str!="") {
str+=sign1[rand()%4];
}
}
}
str+="=";
return str;
}
//根据id,年级,数量,生成对应试卷,并查重当前id文件夹下已有题目
void generate::generate_test(string id,string grade,int amount) {
string alltxt=generate_path(id,0);
string timetxt=generate_path(id,1);
ofstream writeAll;
ofstream writeTime;
string ans="";
string disorder="";
for(int i=0; i<amount; i++) {
string temp=generate_expression(grade);
if(this->myset.insert(temp).second) {
ans+=" "+to_string(i+1)+". "+temp+"\n\n";
disorder+=temp+"\n\n";
} else i--;
}
writeAll.open(alltxt,ios::out|ios::app);
writeAll<<disorder;
writeTime.open(timetxt,ios::out|ios::app);
writeTime<<ans;
writeAll.close();
writeTime.close();
cout<<"题目已生成!请查看txt文件\n"<<endl;
}
四、优缺点分析
1.优点
- 整个程序以功能来划分各个模块,代码较清晰易懂,逻辑条理清楚。各个模块之间使用调用接口的方式实现连接,使得整体代码低耦合、高内聚,可扩展性较高。对比起来自己实现的版本就显得划分不够清晰。
- 随机生成试题模块的核心函数设计巧妙,使用了多个变量(例如记录左右括号之间的跨度、记录操作数变化等)去规范一条算术题的产生,从而也代替了使用多层循环,减少迭代提高了效率。
- 文件读写掌握熟练,将用户信息保存在本地文件而非直接写入代码,避免了修改用户信息时改动代码重新编译,而自己懈怠了没有实现,值得学习。
- 整体代码较为规范,体现在变量命名、有合适的空行来区分函数体的逻辑片段。加了许多注释使得代码的可读性很高。程序的界面也较为简洁美观。用户体验好,而我只实现了文档上的基本需求。
2.不足之处
- 项目需求中要求“在登录状态下,如果用户需要切换类型选项,命令行输入‘切换为xx’ ”即可改变出题类型,而队友使用的是菜单的方式,需要先进入切换功能页面才能改变出题类型。同时,输入”-1“导致在主菜单和出题、切换这三个页面中体现出不一样的结果,前者是退出登录,后两者是返回上一级。此外对需求的理解有一定偏差,如用户名和密码应该同时输入,以空格隔开而非分别输入。
- 登陆之后的菜单界面输入指令标号以及出题时输入数字,如果输入的不是数字而是字符串,程序就会无限循环(崩溃)。经查看源码后发现,输入变量设置为int类型,没有对输入内容若为字符串做处理。
- 生成题目没有将首尾多余的括号去除,可以进一步改进。
- 类中定义全部是public,安全性不高。最好可以把一些例如用户名、密码等变量设置为private。
