你的位置:首页 > 信息动态 > 新闻中心
信息动态
联系我们

结对编程——队友项目代码分析

2022/9/13 19:10:01

一、简介

本博客是对结对编程队友ly的个人项目代码的分析和总结

  • 项目内容:中小学数学卷子自动生成程序
  • 实现语言:C++
  • 代码结构:按功能封装代码,分登陆、显示菜单、随机生成三大类功能来完成该程序的构建。

二、代码运行结果

1.登陆

提示用户输入账号名和密码。
image

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

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

输入“3”可以清屏,输入“-1"则退出登陆。

2.菜单

菜单栏有四项选择,主要功能模块是【1】和【2】。

【1】出题
image

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

image

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

【2】切换试题难度
image

image

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

切换题目难度后,提示输入出题数量,输入范围内数字即可生成试卷txt文件。

输入“-1”返回上一级菜单。

3.查看生成文件

  • 小学
    image

  • 初中
    image

  • 高中
    image

随机题目均符合项目需求。

三、功能代码分析

三大模块均由为.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。