模拟实现计算机学院学生选课绩管理系统。根据要 求每个学生要在每学期选修够20学分,每门课程设置选课人数上限 为60人,每门课程学分设置上限4分,超出限制将无法再被选。
系统功能图如下:
1、设计类要求:
学生类(描述学生专业、班级、学号、姓名、等课程信息 查看选课基本信息);
课程类(描述课程信息:课程编号、课程名称、学分、 人数上限、授课教师等) ;
学生课程类(学生类和课程类所派生,描 述学生选课信息:包括学生编号、姓名、课程编号、课程名称、累 计学分等)。
2、功能要求:
(1)分角色登录:学生和管理员按照账号密码登录。
(2)管理员模块:
①维护学生信息,包含信息的增加、删除、修改、查看等。
②维护课程信息,包含信息的增加、删除、修改、查看等。
③实现学生选课信息查询,包含按照课程名称查询、按照学生名字 查询等。
(3)学生模块:
①可查看备选课程信息,并按照课程编号选课。
②查看已选课程信息和累计学分。
(4)学生信息、课程信息、学生选课信息分别用文件保存,每个 类一个文件,程序执行时从文件读入。 (5)数据要求:学生不少于30条记录,课程信息不少于10条记录, 选课信息不少于20条记录。实验数据要格式规范且合理。
(6)显示要求:人机交互设计要合理友好,便于使用。
很明显,这是我们对面向对象编程有一定了解,同时能够解决数据存储问题,在这里博主使用的是传说中的TXT文件来实现数据存储(因为博主自身不了解数据库),同时为了实现良好的用户界面,博主直接决定使用控制台做终端(还是因为太菜)。
项目总流程图
类的设计部分按照他的要求进行设计即可。下面是我的Student类源代码:
class Student : public User
{
public:
// Student类构造函数
Student() = default;
Student(std::string m, std::string clNum, std::string sID, std::string name, std::string password = "123") :
m_major(m), m_classNum(clNum), m_studentID(sID), m_name(name), m_password(password) {
}
// 返回学生专业
std::string GetMajor() const
{
return m_major;
}
// 返回学生班级
std::string GetClassNum() const
{
return m_classNum;
}
// 返回学生密码
std::string GetPassword() const
{
return m_password;
}
// 返回学生学号
std::string GetID() const
{
return m_studentID;
}
// 返回学生姓名
std::string GetName() const
{
return m_name;
}
private:
// 专业
std::string m_major;
// 班级
std::string m_classNum;
// 学号
std::string m_studentID;
// 姓名
std::string m_name;
// 登录密码
std::string m_password;
};
因为我想要实现数据封装,所以所有的字段皆为private属性,其实可以不用。而父类User类型其实是个空类型,原计划要实现Student学生类和Manager管理员类的多态性(加分),后来发现完全多此一举。
其余各类由于篇幅影响请直接下载源代码查看。再次不再列出。
数据结构我使用的是C++STL vector,并且进行了再封装,非常的朴实无华。
下面的是定义数据结构的一部分代码示例:
// 以StudentData类举例,其仅仅是std::vector data的再封装
class StudentData
{
public:
// 保存所有的学生信息
std::vector<Student> data;
};
// 数据容器
static StudentData studentData;
static ManagerData managerData;
static CourseData courseData;
static StudentCourseData scData;
由于数据有增删的可能性,而且一个小实验我也不用考虑大量数据所带来的各种性能问题。这里我直接在程序开始运行时将保存在TXT文件中的所有数据读取到内存中。下面是我的main函数片段以及ReadFiles函数定义:
int main()
{
const User* user = NULL;
// 获取文件数据
ReadFiles();
// 。。。。。。
void ReadFiles()
{
CopyStudentData();
CopyManagerData();
CopyCourseData();
CopyStudentCourseData();
}
在程序的一开始,运行ReadFiles函数来读取并Copy所有的数据,下面是其中的CopyStudentData函数定义,用于拷贝文件中的所有数据到vector:
// 用于拷贝文件中的学生数据
void CopyStudentData()
{
std::fstream fData;
fData.open("StudentData.txt", std::ios::in);
std::string data1;
std::string data2;
std::string data3;
std::string data4;
std::string data5;
if (fData.is_open())
{
while (!fData.eof())
{
fData >> data1;
fData >> data2;
fData >> data3;
fData >> data4;
fData >> data5;
Student temp(data1, data2, data3, data4, data5);
studentData.data.push_back(temp);
}
}
else
{
std::cout << "No file" << std::endl;
}
}
CopyStudentData函数先打开工作目录下的StudentData.txt文件,再根据Student类的字段分别将读取的数据保存到各个临时变量中。再将各个临时变量的数据构造一个临时的Student对象,最后将数据拷贝到数据结构中。如果我当时要是再聪明一点的话。我完全可以将输入输出运算符(">>“和”<<")在各个数据类中进行重载,但当时我还没有考虑这个代码优化选项。所以代码略显臃肿。
再搞定了数据读取以后,我们就要正式进入选课系统。下面是main函数中选课系统的总览:
// 用户登录
// 返回值:bool isStudent
if (Register(studentData, managerData, &user))
{
// 如果用户是学生
Student temp = *(Student*)(user);
StudentSystem(studentTools, courseData, scData, temp);
SaveData();
}
else
{
// 如果用户是管理员
Manager temp = *(Manager*)(user);
ManagerSystem(managerTools, studentData, managerData, courseData, scData, temp);
SaveData();
}
进入系统的用户进入系统的第一件事情是登录,关于登录系统,以下是我的想法:
首先,用户自行选择自己的身份(学生or管理员),然后系统将要求它输入自己的账号密码(学号密码),接着将输入和数据中的账号密码相比对。如果找到了一样的账号并且密码正确则进入系统,否则请求用户重新输入。和所有的登录过程大同小异。下面是Register函数的定义:
// 登录系统
bool Register(const StudentData & sData, const ManagerData & mData, const User*(* user))
{
//根据选择结果来调用相应的登录系统模块
bool isStudent = Log();
if (isStudent)
{
// 传递登录时填写的用户信息
*user = StudentRegister(sData);
}
else
{
// 传递登录时填写的用户信息
*user = ManagerRegister(mData);
}
return isStudent;
}
分别将学生的全部数据(sData),管理员的全部数据(mData),以及当前用户数据指针(user)。在Log函数中询问用户身份,返回一个能表示身份信息的bool值。根据该bool值来决定调用的是学生登录系统还是管理员登录系统,最后将user指针指向函数返回的数据信息,注意该信息是数据结构中的数据的指针,而user指针是Student类和Manager类的父类User类的指针,所以在这里我能够只用一个指针来指向两个不同类型。
在选择身份之后,接下来就是输入用户的账号密码并且与数据进行比对了。在这里其实学生登录和管理员登录大同小异,下面就只展示StudentRegister学生登录函数:
// 学生登录模块
const Student* StudentRegister(const StudentData & data)
{
std::cout << "你好, 请输入学号:" << std::endl;
std::string stuID; //用于临时存储学号信息
std::cin >> stuID;
const Student* temp;
// 查找该学生
temp = FindStudent(stuID, data);
std::cout << "请输入登录密码:" << std::endl;
while (true)
{
std::string password;
std::cin >> password;
std::cin.get();
// 验证密码
if (Verification(*temp, password))
{
std::cout << "欢迎进入学生系统!" << std::endl;
break;
}
else
{
std::cout << "密码错误,请重新输入" << std::endl;
}
}
return temp;
}
先调用FindStudent函数来查找该学生,FindStudent函数如下:
// 查找学生是否存在
const Student* FindStudent(const std::string temp, const StudentData& data)
{
// 用于返回值
const Student* stemp = NULL;
while (true)
{
bool is_find = false;
auto end = data.data.end();
for (auto i = data.data.begin(); i < end; i++)
{
if (i->GetID() == temp)
{
stemp = &(*i);
is_find = true;
std::cout << "你好 " + i->GetName() << std::endl;
break;
}
}
if (is_find)
{
return stemp;
}
else
{
std::cout << "没有找到该用户,请重新输入" << std::endl;
}
}
}
FindStudent函数用for循环遍历所有数据,比较他们的studentID(学号)信息是否和用户输入一致,如果有一致的数据,则返回该数据的指针,如果没有找到则会请求用户再次输入。
接下来按照找到的数据信息验证密码是否一致,下面是Verification函数定义:
// 验证学生密码
bool Verification(const Student & user, const std::string password)
{
if (user.GetPassword() == password)
{
return true;
}
else
{
return false;
}
}
直接将指针传入函数,获得该学生的密码与用户输入密码进行比对。返回bool值来表示成功与否。函数外的while(true)循环确保用户输入正确密码才可以跳出函数,否则系统会一直请求用户输入密码。
管理员登录函数(ManagerRegister)函数与学生登录函数(StudentRegister)原理大同小异,注意管理员登录函数中调用的函数是重载过的。
在成功登录以后,系统根据身份分别调用StudentSystem或者ManagerSystem,来进入系统主功能区。这里有一个小细节,user指针被强制转换为对应的数据类型的指针,再解引用得到数据对象并拷贝对象数据到临时对象,接下来将临时对象数据传递给函数参数。
// 如果用户是学生
Student temp = *(Student*)(user);
StudentSystem(studentTools, courseData, scData, temp);
// 如果用户是管理员
Manager temp = *(Manager*)(user);
ManagerSystem(managerTools, studentData, managerData, courseData, scData, temp);
回顾一下系统的功能:
学生模块:
管理员模块:
为了实现以上功能,除了一个一个的实现对应的函数,还要让用户能够选择自己想要进行的操作。我们先实现功能的选择。下面为StudentSystem函数定义:
// 学生系统选项
void StudentSystem(StudentModuleTools tools, CourseData& cData, StudentCourseData& scData, Student s)
{
while (true)
{
std::cout << "请选择你接下来的操作:" << std::endl;
std::cout << "A - 查看备选课程信息" << std::endl;
std::cout << "B - 按照课程编号选课" << std::endl;
std::cout << "C - 查看已选课程信息和累计学分" << std::endl;
std::cout << "D - 退出系统" << std::endl;
char a = std::cin.get();
std::cin.get();
if (a == 'A' || a == 'a')
{
tools.CheckOptionalCourses(cData);
}
else if (a == 'B' || a == 'b')
{
tools.SelectCourse(cData, scData, s);
}
else if (a == 'C' || a == 'c')
{
tools.CheckSelected_n_totalCred(scData, s);
}
else if (a == 'D' || a == 'd')
{
break;
}
else
{
std::cout << "请输入正确的字符" << std::endl;
}
std::cout << std::endl;
}
}
在StudentSystem函数中,我们会要求用户根据给出的选项进行选择,每一个选择都会调用对应的功能函数,除非用户选择了退出系统,否则while(true)会保证程序一直运行。在输入错误选项之后程序会输出错误提醒,并再次进行循环。其中选择代码可以用switch语句优化,同时我们的ManagerSystem函数和Student System函数结构上大同小异(只不过因为功能更多使用更复杂)。详细请查看源代码。
当用户选择退出系统之后,程序会退出主功能区,并执行SaveData函数,最后程序退出。SaveData函数在退出前将内存中的数据(无论有没有出现过更改)重新输入到文件中。SaveData函数可以理解为ReadFiles函数的逆操作,下面是SavaData函数的定义:
// 保存数据
void SaveData()
{
SaveStudentData();
SaveCourseData();
SaveManagerData();
SaveStudentCourseData();
}
在SaveData函数中我们一次对所有的数据都进行了保存。因为本来就不在乎性能嘛。
下面举例说明一下SavaStudentData的源代码,SaveStudentData函数的定义如下:
// 保存所有学生数据
void SaveStudentData()
{
std::fstream fData;
fData.open("StudentData.txt", std::ios::out);
std::string data1;
std::string data2;
std::string data3;
std::string data4;
std::string data5;
if (fData.is_open())
{
for (size_t i = 0; i < studentData.data.size(); i++)
{
data1 = studentData.data[i].GetMajor();
data2 = studentData.data[i].GetClassNum();
data3 = studentData.data[i].GetID();
data4 = studentData.data[i].GetName();
data5 = studentData.data[i].GetPassword();
fData << data1;
fData << " ";
fData << data2;
fData << " ";
fData << data3;
fData << " ";
fData << data4;
fData << " ";
fData << data5;
if(!(i + 1 == studentData.data.size()))
fData << std::endl;
}
}
else
{
std::cout << "No file" << std::endl;
}
fData.close();
}
由于是ReadFiles函数的逆操作,所有有的地方不再说明,需要说明的是,打开文件的模式会在打开文件时清楚文件中的所有数据,这样虽然听起来不是特别好,但是其实可以解决一些bug,而且确实是可行的。写入时保证格式就可以了。
以上就是学生选课系统的程序流程。接下来是功能的实现和大概的说明
即使功能有好几种,流程也可以简化为:
获取相应数据 -> 对数据做操作
当然了,如果数据修改了,也一定要反应到内存中的数据结构中,这样才能在程序退出时将修改保存到文件中。
下面举出学生选课的实现方法:
// 按照课程编号选课
void StudentModuleTools::SelectCourse(CourseData& cData, StudentCourseData& scData, const Student& user)
{
std::cout << std::endl;
std::cout << "请输入课程编号" << std::endl;
std::string courseNumTemp;
std::cin >> courseNumTemp;
std::cin.get();
for (size_t i = 0; i < cData.data.size(); i++)
{
// 查找该课程编号
if (cData.data[i].GetCourseNum() == courseNumTemp)
{
// 验证该课程是否已满
if(!cData.data[i].isFull())
{
// 查找到该用户的学生选课数据
StudentCourse* user_sc = NULL;
for (size_t c = 0; c < scData.data.size(); c++)
{
if (scData.data[c].GetID() == user.GetID())
{
// 如果找到了,添加引用
user_sc = &(scData.data[c]);
}
}
// 检查选课是否重复
bool isSelected = false;
for (size_t i = 0; i < user_sc->selectedCourses.size(); i++)
{
// 如果要选择的课程已经被选择
if (courseNumTemp == (*user_sc).selectedCourses[i].GetCourseNum())
{
isSelected = true;
}
}
// 如果没有重复
if (!isSelected)
{
std::cout << "已选择课程:" << std::endl;
std::cout << cData.data[i].GetCourseNum()
<< " "
<< cData.data[i].GetCourseName()
<< std::endl;
// 将课程的已选人数加一
cData.data[i].AddOnePopulation();
// 将学生选课的总选课数加一
user_sc->AddOneSeletedCourseNum();
// 在学生课程数据中添加该选课信息
user_sc->selectedCourses.push_back(cData.data[i]);
}
else
{
std::cout << "该课程已经被选择过了" << std::endl;
break;
}
}
else
{
std::cout << "该课程已满" << std::endl;
break;
}
}
}
}
我们的流程是一样的,先根据用户输入的学号找到数据结构中对应的数据,在此情况下为课程,然后就是进行操作了,具体操作为:
对于数据的增减,无论增加的数据是什么,只需要增加就行了,注意如果增加学生信息,还需要再增加相应的学生选课信息
下面是管理员增加学生信息方法:
// 增加学生信息
void ManagerModuleTools::Manage_Add(StudentData& sData, StudentCourseData& scData)
{
// 想用户获取信息
std::cout << "请输入要添加的学生信息:" << std::endl;
std::string data1;
std::string data2;
std::string data3;
std::string data4;
std::string data5;
std::cout << "请输入专业:";
std::cin >> data1;
std::cout << "请输入班级:";
std::cin >> data2;
std::cout << "请输入学号:";
std::cin >> data3;
std::cout << "请输入姓名:";
std::cin >> data4;
std::cout << "请输入登录密码:";
std::cin >> data5;
std::cin.get();
// 将获得的信息用于构造新的数据对象
Student stemp(data1, data2, data3, data4, data5);
StudentCourse sctemp(data1, data2, data3, data4);
// 将新数据添加到数据容器中
sData.data.push_back(stemp);
scData.data.push_back(sctemp);
std::cout << "添加成功!" << std::endl;
}
对于信息的删减和修改,需要注意一点,删减或者修改之后极可能对现在数据结构中的其他数据照成影响,比如说我删了一个课程信息,那么数据库中所有选择了该课程的学生选课信息就同样要删减掉该课程,同时已选课程减一。如果是修改课程信息,比如说该课程老师变动,则不需要,但是如果是最大选课数量变了,根据我的理解如果变动以后发现已选择的人数比修改后的最大人数多了,公平起见应该也要删除所有学生选课信息中的该课程信息,并让他们重新选择。
下面是管理员的删除课程信息方法:
// 删除课程信息
void ManagerModuleTools::Manage_Delete(CourseData& cData, StudentCourseData& scData)
{
std::cout << "输入要删除课程的课程号" << std::endl;
std::string courseNum;
std::cin >> courseNum;
std::cin.get();
// 删除所有学生选课中本课程的记录
for (size_t i = 0; i < scData.data.size(); i++)
{
// 如果该学生选了课
if (scData.data[i].GetSelectedCourseNum() > 0)
{
// 查找其中是否有该课程
for (size_t c = 0; c < scData.data[i].selectedCourses.size(); c++)
{
// 如果有,删除选课记录
if (courseNum == scData.data[i].selectedCourses[c].GetCourseNum())
{
auto iter = scData.data[i].selectedCourses.begin() + c;
scData.data[i].selectedCourses.erase(iter);
scData.data[i].MinusOneSeletedCourseNum();
break;
}
}
}
}
// 在课程数据容器中找到该课程
for (size_t i = 0; i < cData.data.size(); i++)
{
if (courseNum == cData.data[i].GetCourseNum())
{
// 删除它
auto iter = cData.data.begin() + i;
cData.data.erase(iter);
break;
}
}
std::cout << "删除成功!" << std::endl;
}
最后就是最简单的数据查找,只需要按照要求实现就可以了,因为管理员查找方法需要两种查找方式,所以一开始会请求用户进行选择。
管理员查找方法如下:
// 查找学生信息
void ManagerModuleTools::Manage_CheckStudentData(const StudentData& sData)
{
while (true)
{
// 是查看全部还是查看个别
std::cout << "A - 查看全部" << std::endl;
std::cout << "B - 查看单个" << std::endl;
char a = std::cin.get();
std::cin.get();
if (a == 'A' || a == 'a')
{
for (size_t i = 0; i < sData.data.size(); i++)
{
std::cout << sData.data[i].GetMajor()
<< " "
<< sData.data[i].GetClassNum()
<< " "
<< sData.data[i].GetID()
<< " "
<< sData.data[i].GetName()
<< " "
<< sData.data[i].GetPassword()
<< std::endl;
}
break;
}
else if (a == 'B' || a == 'b')
{
std::cout << "请输入要查找的学生学号" << std::endl;
std::string stuID;
std::cin >> stuID;
std::cin.get();
// 找到该用户
const Student* p_temp = NULL;
while (true)
{
bool is_find = false;
for (size_t i = 0; i < sData.data.size(); i++)
{
if (stuID == sData.data[i].GetID())
{
is_find = true;
p_temp = &(sData.data[i]);
}
}
if (is_find)
{
break;
}
else
{
std::cout << "没有找到该用户,请重新输入" << std::endl;
}
}
std::cout << "专业:" << p_temp->GetMajor()
<< std::endl
<< "班级:" << p_temp->GetClassNum()
<< std::endl
<< "学号:" << p_temp->GetID()
<< std::endl
<< "姓名:" << p_temp->GetName()
<< std::endl
<< "登录密码:" << p_temp->GetPassword();
break;
}
else
{
std::cout << "请输入正确字符!" << std::endl;
}
}
}
以上就是博主本人的学生选课系统的实现,由于是核心代码,并且由于博主自身水平限制以及摸鱼严重,所以终端使用的是最原始的控制台,并且代码比较臃肿,如果你有需要的话,完全可以很轻松的对代码进行优化。
运行示例: