1、演讲比赛流程管理系统
1.1 比赛规则
- 学校举行一场演讲比赛,共有 12 个人参加。比赛共两轮,第一轮为淘汰赛,第二轮为决赛。
- 每名选手都有对应的编号,如 10001~10012
- 比赛方式:分组比赛,每组 6 个人;
- 第一轮分为两个小组,整体按照选手编号进行抽签后顺序演讲。
- 十个评委分别给每名选手打分,去除最高分和最低分,求的平均分为本轮选手的成绩
- 当小组演讲完后,淘汰组内排名最后的三个选手,前三名晋级,进入下一轮的比赛。
- 第二轮为决赛,前三名胜出
- 每轮比赛过后需要显示晋级选手的信息
1.2 程序功能
- 开始演讲比赛:完成整届比赛的流程,每个比赛阶段需给用户提示,用户按任意键后继续下一个阶段。
- 查看往届记录:查看之前比赛前三名结果,每次比赛记录到.csv 后缀的文件中。
- 清空比赛记录:清空文件中的数据。
- 退出比赛程序:退出当前程序。
运行环境:Visual Studio 2022,X64,Debug
整体思路与之前写过的博客类似:利用C++多态完成职工管理系统
https://blog.csdn.net/zhendeshini_/article/details/149124809
技术要求:
- 掌握C++的指针、数组、函数的用法,程序流程结构if、switch、for语句的用法
- 对封装有基本了解
- 熟悉
vector、map、deque
等容器的基本用法 - 文件的加载与保存
- 分文件编写的用法
1.3 创建管理类
在这里实现所有功能的访问,先绘制好大致框架,后续需要添加的在这里添加即可,采用分文件编写
头文件完成类的声明,包含函数的构造、声明与析构
speechManager.h
:
//speechManager.h
#pragma once
#include<iostream>
using namespace std;
//设计演讲管理类
class SpeechManager {
public:
//构造函数声明
SpeechManager();
//展示菜单
void showMenu();
//退出系统
void exitSystem();
//开始演讲比赛
void startSpeech();
//显示往届记录
void showRecord();
//清空比赛记录
void clearRecord();
//析构函数声明
~SpeechManager();
};
speechManager.cpp
//speechManager.cpp
#include "speechManager.h"
//函数实现
//构造函数定义
SpeechManager::SpeechManager() {}
//展示菜单
void SpeechManager::showMenu() {}
//退出系统
void SpeechManager::exitSystem() {}
//开始演讲比赛
void SpeechManager::startSpeech() {}
//显示往届记录
void SpeechManager::showRecord() {}
//清空比赛记录
void SpeechManager::clearRecord() {}
//析构函数定义
SpeechManager::~SpeechManager() {}
在写出对应的main
函数:
演讲比赛管理系统.cpp
//这是演讲比赛管理系统.cpp的实现
#include "speechManager.h"
int main() {
SpeechManager sm;//实例化对象
system("pause");
return 0;
}
总结:在开始阶段先写好这些功能的函数的声明、对应的函数实现、main函数的创建,最后实例化对象,用来访问所有功能。框架已经整体搭建完毕,接下来实现其中的具体功能。
1.4 演讲者类
将演讲者创建为一个类,添加人的属性,由于比赛是两轮竞赛,所以创建两个数组,用来存放第一轮、第二轮的得分。
speaker.h
//speaker.h
#pragma once
#include <iostream>
using namespace std;
class Speaker {
public:
string m_Name;
double m_Score[2];//两轮得分,创建一个数组
};
1.5 交互界面
大体框架实现后我们接下来我们来实现一个互动性的功能。
思路:在显示菜单实现上面有数字提示,我们可以利用switch语句实行多条件分支语句
显示菜单界面实现:
speechManager.cpp
//展示菜单
void SpeechManager::showMenu() {
cout << "******************************************" << endl;
cout << "************* 欢迎参加演讲比赛 ************" << endl;
cout << "************* 1.开始演讲比赛 **************" << endl;
cout << "************* 2.查看往届记录 **************" << endl;
cout << "************* 3.清空比赛记录 **************" << endl;
cout << "************* 0.退出比赛程序 **************" << endl;
cout << "******************************************" << endl;
cout << endl;
}
//退出系统
void SpeechManager::exitSystem() {
cout << "欢迎下次再来:" << endl;
system("pause");
exit(0);
}
演讲比赛管理系统.cpp
//这是演讲比赛管理系统.cpp的实现
#include "speechManager.h"
int main() {
SpeechManager sm;
int select = 0;
while (true) {
sm.showMenu();
cout << "请输入你的选择:" << endl;
cin >> select;
switch (select) {
case 1://开始比赛
sm.startSpeech();
break;
case 2://查看往届记录
sm.showRecord();
break;
case 3://清空比赛记录
sm.clearRecord();
break;
case 0://退出系统
sm.exitSystem();
break;
default://清屏操作
system("cls");
break;
}
}
system("pause");
return 0;
}
1.6 功能分析
比赛流程:
第一轮:抽签-开始演讲比赛-显示第一轮比赛结果
第二轮:抽签-开始演讲比赛-显示前三名结果-保存结果到文件。
-
在开始之前我们先想一个问题,目前有哪些数据类型?
答:目前有speaker(m_Name,m_Score[2])
这一数据类型,还缺少编号这一数据类型。 -
如何找到这些人呢?关联多轮比赛数据呢?
显然根据姓名与分数都不合适,姓名可能有重复的,分数是随着轮数变化的,所以只剩下了静态属性编号,这是唯一的。 -
编号放在哪里?
如果将编号放入类中,不使用容器,那么创建对象时需要手动创建类的实例化吗,数量固定Speaker s1 = {1, "张三", 0}; Speaker s2 = {2, "李四", 0};Speaker s3 = {3, "王五", 0};
,抽签的顺序也得需要手动设置
如n个人需要n!种可能所以需要大量代码,而且代码不能处理不确定性,人数变化,无法通过循环来完成批量处理,鲁棒性较差。所以最终选择容器,里面有随机打乱,可以批量创建,支持数据解耦如map容器map<int,Speaker>
可以将编号与Speaker数据类型解藕,不像前者一样僵硬。 -
编号问题解决了,如何解决编号与人之间的强关联性、唯一性?
编号是
int
数据类型,选手是Speaker
类型,想到了map容器的key与value,键值对的关系,这样就建立起了强关联性、唯一性。
针对以上问题,我们建立以下容器,分别进行第一轮、第二轮、最总获胜者的编号管理容器vector
,以及存放信息的容器map
。
speechManager.h
//speechManager.h
#include <vector>
#include <map>
……
//成员属性:
//保存第一轮比赛选手编号容器
vector<int>v1;
//保存第二轮比赛选手编号容器
vector<int>v2;
//显示最终胜利者容器
vector<int>vVictory;
//存放编号以及对应具体选手编号
map<int, Speaker>m_Speaker;
//存放比赛轮数
int m_Index;
1.7 开始比赛
可以看出开始比赛要完成的功能较多,所以将开始比赛作为一个大函数,里面封装具体的子功能函数
教程中是单独分两次进行比赛的,但是如果轮数变多之后,考虑到代码的可维护性,我们将其变为循环,整体框架如下:
创建选手-抽签-比赛-显示结果-下一轮-保存结果-重置比赛状态。
// 开始比赛
void SpeechManager::startSpeech() {
// 比赛轮次循环
for (int round = 0; round < 2; round++) {
// 1、抽签
this->speechDraw();
// 2、比赛
this->speechContest();
// 3、显示结果
this->showScore();
// 准备下一轮
if (round == 0) {
this->m_Index++; // 只有从第一轮进入第二轮时需要更新索引
}
}
// 4、保存结果到文件
this->saveFile();
// 重置比赛状态,为下一届比赛做准备
this->initSPeech();//清除容器,重置轮次索引,清除当前届比赛
this->createSpeaker(); //重新创建 12 名新选手
this->loadFile();//重新加载往届记录
cout << "本届比赛完毕!" << endl;
system("pause");
system("cls");
}
重置的目的:
initSPeech()
:清空容器(v1
、v2
、vVictory
、m_Speaker
),重置轮次索引(m_Index=1
),清除当前届比赛数据。createSpeaker()
:重新创建 12 名新选手(否则选手 ID 会重复递增,导致冲突)。loadFile()
:重新加载往届记录(确保m_Record
反映最新文件状态,如用户在比赛后清空了记录)。
1.7.1 初始化属性
比赛开始之前先将属性进行初始化
speechManager.cpp
:
//speechManager.cpp
#include "speechManager.h"
//函数实现
//构造函数定义
SpeechManager::SpeechManager() {
//初始化容器和属性
this->initSPeech();
}
//初始化容器和属性
void SpeechManager::initSPeech() {
//容器都置空
this->v1.clear();//第一轮容器置空
this->v2.clear();//第二轮置空
this->vVictory.clear();//最终结果容器置空
this->m_Speaker.clear();//存放编号以及个人信息
this->m_index = 0;//轮数置0
}
//析构函数定义
SpeechManager::~SpeechManager() {}
1.7.2 创建选手
//speechManager.cpp
#include "speechManager.h"
//函数实现
//构造函数定义
SpeechManager::SpeechManager() {
//初始化容器和属性
this->initSPeech();
this->createSpeaker();
}
……
//创建选手
void SpeechManager::createSpeaker() {
//创建对象
string nameSeed = "ABCDEFGHIJKL";
for (int i = 0; i < nameSeed.size(); i++) {
Speaker speaker;
//将姓名初始化
speaker.m_Name = string("选手")+ nameSeed[i];
//得分初始化
for (int j = 0; j < 2; j++) {
speaker.m_Score[j] = 0.0;
}
//12名选手编号,放入v1
this->v1.push_back(i + 10001);
//将编号与演讲者插入map,以键值对方式
//map<int, Speaker>m_Speaker;已经创建好的map容器,忘记后vsStudio会智能提示注释
this->m_Speaker.insert(make_pair(i + 10001, speaker));
}
}
测试结果如下:
演讲比赛管理系统.cpp
SpeechManager sm;
//sm.createSpeaker();
//for (map<int, Speaker>::iterator it = sm.m_Speaker.begin(); it != sm.m_Speaker.end(); it++) {
// cout << "编号:" << it->first
// << "\t姓名:" << it->second.m_Name
// << "\t分数:" << it->second.m_Score[0] << endl;
//}
1.7.3 抽签
流程中进行两轮抽签
利用记录比赛轮数的轮数作为判断依据,将对应的容器编号进行打乱顺序。
//抽签
void SpeechManager::speechDraw() {
cout << "第" << m_Index << "轮选手正在抽签" << endl;
cout << "--------------------------------" << endl;
cout << "抽签后的演讲顺序如下:" << endl;
if (this->m_Index == 1) {
//第一轮比赛
random_shuffle(v1.begin(), v1.end());//将第一轮容器随机打乱
for (vector<int>::iterator it = v1.begin(); it != v1.end(); it++) {
cout << *it << " ";
}
cout << endl;
}
else {
//第二轮比赛
random_shuffle(v2.begin(), v2.end());//将第二轮容器随机打乱
for (vector<int>::iterator it = v2.begin(); it != v2.end(); it++) {
cout << *it << " ";
}
cout << endl;
}
cout << "--------------------------------" << endl;
system("pause");
cout << endl;
}
1.7.4 演讲比赛
思路:在前面的功能分析中,我们可以建立了第一轮容器v1
、第二轮容器v2
、最总获胜者的编号管理容器vector
,以及存放编号的Speaker
信息的容器map
。
- 应该直接对原容器进行处理吗?
答:要对容器中的编号进行随机打乱,排序等等操作,如果直接使用原容器进行排序删除等操作,会破坏原来容器的内容,比如删除掉一个人,到下一届比赛时,人数就减少了,得重新初始化容器,不如直接copy一个副本,这样与那容器得数据始终保持初始状态,后续重复复用得时候,对副本进行修改即可,不会影响到原容器,数据安全性高。
生活实际例子:老板发给你的原版word文件让员工去修改,员工第一时间想到是的先下载原版初稿,然后多复制几份如1.0,1.1等等,在副本基础上进行修改,如果老板对1.0,1.1很不满意,可以打开原版初稿查看,保留了原有的版本,方便在原有基础进行重新修改。
下面来看一下关于演讲比赛的代码:
void SpeechManager::speechContest(){}
//比赛
void SpeechManager::speechContest() {
cout << "-------------第" << this->m_Index << "轮比赛正式开始--------------" << endl;
int num = 0;//用于统计记录人员个数,6人一组
vector <int>v_Src;//比赛选手容器
if (this->m_Index==1) {
//执行第一轮
v_Src = v1;
}
else{
//执行第二轮
v_Src = v2;
}
//遍历所有的选手进行比赛
for (vector<int>::iterator it = v_Src.begin(); it != v_Src.end(); it++) {
num++;
//评委打分
deque<double>d;
for (int i = 0; i < 10; i++) {
double score = (rand() % 401 + 600) / 10.f;
//cout << score <<" ";
d.push_back(score);
}
sort(d.begin(), d.end(),greater<double>());//降序排列
d.pop_front();//去除最高
d.pop_back();//去除最低
double scoreSum = accumulate(d.begin(), d.end(),0.0f);//起始,终止,初始值
double avg = scoreSum /(double) d.size();//小数/小数
//"打印平均分"
//cout << "编号:" << *it << "\t姓名:" << this->m_Speaker[*it].m_Name
// << "\t分数:" << avg ;
//将平均分放入map容器
this->m_Speaker[*it].m_Score[this->m_Index - 1] = avg;
//准备临时容器存放小组成绩(降序排列)
multimap<double, int, greater<double>>groupScore;//让分数作为键进行排序,编号是值
//将打分数据放入到临时小组容器中
groupScore.insert(make_pair(avg, *it));//均分作为键,value是具体选手编号
//每六人取出前三名
if (num % 6 == 0) {
cout << "第" << num / 6 << "小组名次:" << endl;
for (multimap<double, int, greater<double>>::iterator it = groupScore.begin(); it != groupScore.end(); it++) {
cout << "编号:" << it->second << "\t姓名:" << this->m_Speaker[it->second].m_Name
<< "\t分数:" << this->m_Speaker[it->second].m_Score[this->m_Index-1] << endl;
}
//取走前三名
int count = 0;
for (multimap<double,int , greater<double>>::iterator it = groupScore.begin(); it != groupScore.end()&&count<3; it++,count++) {
if (this->m_Index == 1) {
v2.push_back((*it).second);
}
else {
vVictory.push_back((*it).second);
}
}
groupScore.clear();//小组容器清空
}
}
cout << "-------------------第"<<this->m_Index<<"轮比赛完毕-------------------"<<endl;
system("pause");
}
测试结果:
代码流程图
下面这张可视化图片是对speechContest()
这一函数的逻辑解释,先整体查看数据流向
详细解释
(1) void SpeechManager::createSpeaker()
函数:
创建容器v1,v2,map,进行初始化,填充编号,分数初始化,将编号、Speaker放入map容器,实现数据解耦与强关联(功能分析)。这里key值对应的是编号,value对应的是Speaker,里面存放的是姓名以及两轮分数,实现了唯一性。
当然void SpeechManager::speechDraw()
函数有编号打乱功能,较为简单,序号无固定规律,为了可读性,这里就不展示打乱后的了,同时编号以1-12代替10001-10012。
(2) 创建副本容器v_Src
vector <int>v_Src;//比赛选手容器
if (this->m_Index==1) {
//执行第一轮
v_Src = v1;
}
else{
//执行第二轮
v_Src = v2;
}
(3) 评委打分,更新map容器中的数据
由于评委打的分数要进行去掉最高分、最低分,所以使用sort算法进行降序排列,虽然vector的数据删除功能针对尾部元素效率很高,只需调整尾部指针,无需移动其他元素。但是vector内部是连续内存,导致删除头部元素时较为困难,需要将后续所有的元素进行向前移动,时间消耗较大。而deque内存布局是分段连续的,通过buffer缓冲区进行指针数组管理,对于头部删除不需要移动其他元素。
排序之后经过pop_front
与pop_back
进行掐头去尾,在利用accumulate
实现累加,最后除以个数得到平均分。
将每个人计算好的平均分放入到map容器中,it是key值索引指针,解引用即可得到编号,利用这个编号(key)可以找到map容器中的key值对应得Speaker,在利用Speaker下具体key值即可访问到具体的speaker中的m_Score信息对应这行代码:
this->m_Speaker[*it].m_Score[this->m_Index - 1] = avg;
,但注意此时的排序是为了得到每一个选手掐头去尾的平均分,选手与选手之间的分数还并未排序,这也就是图中红色的部分尚未排序。
同时,map容器中的key值是编号,无法完成按照分数进行排序。
至此,这一步完成了每位选手的平均分,并且存入到map中,达到了唯一性与解耦。
//遍历所有的选手进行比赛
for (vector<int>::iterator it = v_Src.begin(); it != v_Src.end(); it++) {
num++;
//评委打分
deque<double>d;
for (int i = 0; i < 10; i++) {
double score = (rand() % 401 + 600) / 10.f;
//cout << score <<" ";
d.push_back(score);
}
sort(d.begin(), d.end(),greater<double>());//降序排列
d.pop_front();//去除最高
d.pop_back();//去除最低
double scoreSum = accumulate(d.begin(), d.end(),0.0f);//起始,终止,初始值
double avg = scoreSum /(double) d.size();//小数/小数
//"打印平均分"
//cout << "编号:" << *it << "\t姓名:" << this->m_Speaker[*it].m_Name
// << "\t分数:" << avg ;
//将平均分放入map容器
this->m_Speaker[*it].m_Score[this->m_Index - 1] = avg;
(4) 对所有选手得分进行降序排列
上一步我们已经完成了对map容器数据的保存,但是由于key值的原因,导致无法进行所有人的分数排序,所以又面临一个问题是对原容器进行修改还是新copy一个,在功能分析已经解释过这个原因了,所以毫无疑问是副本了。
但是会有新问题,我们知道map容器可以实现自动排序,需要把分数作为key值,也就是互换,所以这次的副本不再是简单的拷贝map了,如何实现呢?我们知道可以利用map容器中的insert功能创建一对新的键值对,将数据类型进行重新调整即可解决新问题。
但又有新问题,按照分数进行排序,分数很有可能会重复,map是不允许有重复的,所以使用允许重复的multimap容器。
所以最终使用的是multimap这个临时容器存储map中的数据,实现自动排序。
对应的代码:
int num = 0;//用于统计记录人员个数,6人一组
//准备临时容器存放小组成绩(降序排列),放在for循环外,要记录6人成绩,否则会导致只记录2人
multimap<double, int, greater<double>>groupScore;//让分数作为键进行排序,编号是值
……
// for循环:
//将打分数据放入到临时小组容器中
groupScore.insert(make_pair(avg, *it));//均分作为键,value是具体选手编号
输出代码这里较为简单,只需要注意一下键值对的位置即可,编号是second
,输出姓名则告诉m_Speaker
的对应编号即可访问到其下的m_Name
;
每六个人进行取走前三名进行下一轮晋级,所以对multimap进行操作即可,*it即可获得编号,将编号插入到下一轮对应的容器,第一轮的结果就插入到第二轮容器v2,第二轮容器就插入到最终容器vVector
。
//每六人取出前三名
if (num % 6 == 0) {
cout << "第" << num / 6 << "小组名次:" << endl;
for (multimap<double, int, greater<double>>::iterator it = groupScore.begin(); it != groupScore.end(); it++) {
cout << "编号:" << it->second << "\t姓名:" << this->m_Speaker[it->second].m_Name
<< "\t分数:" << this->m_Speaker[it->second].m_Score[this->m_Index-1] << endl;
}
//取走前三名
int count = 0;
for (multimap<double,int , greater<double>>::iterator it = groupScore.begin(); it != groupScore.end()&&count<3; it++,count++) {
if (this->m_Index == 1) {
v2.push_back((*it).second);
}
else {
vVictory.push_back((*it).second);
}
}
groupScore.clear();//小组容器清空
}
最后给临时容器清空,方便下一轮循环。
最后展示一下map容器(m_Speaker)
是如何实现与临时容器multimap(groupScore)
实现数据交互的,桥梁是"编号"!
- 测试
main函数:
sm.speechContest();
1.7.5 显示比赛分数
根据m_Index判断处于哪一轮,然后将map容器(m_Speaker)中的信息进行显示,同理迭代器it进行解引用得到编号,利用编号可以访问到m_Speaker唯一对应的信息,只不过需要注意的是m_Score有两个数组,第一轮和第二轮由于Speaker属性是0和1,m_Index是1和2,所以要进行减一即可。
//显示得分
void SpeechManager::showScore() {
cout << "---------------第" << this->m_Index << "轮晋级选手信息如下:-----------------" << endl;
vector<int>v;
if (this->m_Index == 1) {
//第一轮比赛结束
v = v2;
}
else {
v = vVictory;
}
for (vector<int>::iterator it = v.begin(); it != v.end(); it++) {
cout << "编号:" << *it << "\t姓名:" << this->m_Speaker[*it].m_Name
<< "\t分数:" << this->m_Speaker[*it].m_Score[this->m_Index-1] << endl;//不指明具体数组索引会显示数组首地址
}
cout << endl;
system("pause");
system("cls");
this->showMenu();
}
至此演讲比赛这一功能已经实现!
1.8 保存文件
speechManager.h
添加文件相关的函数声明
//保存文件
void saveFile();
//加载文件
void loadFile();
……
//清空文件
void clearRecord();
//判断文件是否为空
bool fileIsEmpty;
函数定义
#define FILENAME "speech.csv"//宏定义 文件名
//保存文件
void SpeechManager::saveFile() {
ofstream ofs;
ofs.open(FILENAME,ios::out|ios::app);//用追加方式写文件
//将每个选手数据写入到文件中
for (vector<int>::iterator it = vVictory.begin(); it != vVictory.end(); it++) {
ofs<< *it <<","<<this->m_Speaker[*it].m_Name <<","<< this->m_Speaker[*it].m_Score[m_Index - 1]<<",";
}
ofs << endl;
ofs.close();
cout << "已经保存完成!" << endl;
//更改文件不为空的状态
this->fileIsEmpty = false;
}
在保存文件这里记得指定文件状态。
加载文件
在speechManager.h
添加记录往届比赛的容器
//往届记录
map<int, vector<string>>m_Record;
//加载文件
void SpeechManager::loadFile() {
//清空m_Record中内存中的遗留数据,不是真实数据,后面会补充
this->m_Record.clear();
ifstream ifs;
ifs.open(FILENAME, ios::in);
// 1、文件不存在,自动创建
if (!ifs.is_open()) {
this->fileIsEmpty = true;
cout << "文件不存在,已自动创建" << endl;
ifs.close();
return;
}
// 2、文件存在,并且没有记录
char ch;
ifs >> ch;
if (ifs.eof()) {
cout << "文件为空" << endl;
this->fileIsEmpty = true;
ifs.close();
return;
}
// 3、文件存在,并且记录数据
this->fileIsEmpty = false;
ifs.putback(ch); // 如果读取了一个字符,将其放回输入流
string line;
int index = 0; // 记录总记录数
while (getline(ifs, line)) { // 按行读取完整数据
vector<string> v; // 存储当前行的所有字段
int pos = 0; // 搜索起始位置
int commaPos = 0; // 逗号位置
// 解析当前行的所有字段(按逗号分割)
while ((commaPos = line.find(',', pos)) != string::npos) {
v.push_back(line.substr(pos, commaPos - pos));
pos = commaPos + 1;
}
v.push_back(line.substr(pos)); // 添加最后一个字段(无逗号后的内容)
// 确保字段数量足够(至少3个:编号、姓名、分数)
if (v.size() >= 3) {
this->m_Record.insert(make_pair(index, v));
index++;
}
else {
cout << "警告:格式错误的行 - " << line << endl;
}
}
ifs.close();
////测试读取结果,以读取每一行第一个为列
//for (map<int, vector<string>>::iterator it = this->m_Record.begin(); it != this->m_Record.end(); it++) {
// cout << "第" << it->first+1 << "届记录" << endl;
// cout << "编号:" << it->second[0] << "\t名字" << it->second[1] << "\t分数" << it->second[2] << endl;
//}
}
第三种情况主要针对这种有记录的,利用一个map容器接受这些数据,key代表每一届的记录,string容器代表从","分割得到的编号,姓名,分数。
1.10 查看往届记录
从csv文件中读取
与上面的测试代码雷士,从m_Record中获取每一行的从头到尾数据。
//显示往届记录
void SpeechManager::showRecord() {
if (this->fileIsEmpty) {
cout << "文件为空或不存在!" << endl;
}
for (int i = 0; i < this->m_Record.size(); i++) {
cout << "第" << i + 1 << "届" << endl;
cout<< "冠军编号" << this->m_Record[i][0]
<< "\t名字:" << this->m_Record[i][1] << "\t分数:" << this->m_Record[i][2] << endl;
cout<< "亚军编号" << this->m_Record[i][3]
<< "\t名字:" << this->m_Record[i][4] << "\t分数:" << this->m_Record[i][5] << endl;
cout << "季军编号" << this->m_Record[i][6]
<< "\t名字:" << this->m_Record[i][7] << "\t分数:" << this->m_Record[i][8] << endl;
}
system("pause");
system("cls");
}
结果如下:
1.11 清空功能
//清空记录
void SpeechManager::clearRecord() {
cout << "是否确定清空操作?" << endl;
cout << "1-是、2-否" << endl;
int select = 0;
cin >> select;
if (select == 1) {
ofstream ofs(FILENAME, ios::trunc);
ofs.close();
//重置
this->initSPeech();
this->createSpeaker();
this->loadFile();
cout << "清空操作" << endl;
}
else {
system("pause");
system("cls");
}
}
最后在管理一下初始化函数:
新增当前记录清空
//初始化容器和属性
void SpeechManager::initSPeech() {
//容器都置空
this->v1.clear();
this->v2.clear();
this->vVictory.clear();
this->m_Speaker.clear();
this->m_Index = 1;
//将记录的容器也清空
this->m_Record.clear();
}
由于没有在堆区开辟新内存,不需要析构函数。
2、完整代码
speechManager.h
//speechManager.h
#pragma once
#include<iostream>
#include <vector>
#include <map>
#include "speaker.h"
#include<algorithm>
#include<deque>
#include<functional>
#include <numeric>
#include <string>
#include<fstream>
#include<string>
#include "ctime"
using namespace std;
//设计演讲管理类
class SpeechManager {
public:
//构造函数声明
SpeechManager();
//展示菜单
void showMenu();
//退出系统
void exitSystem();
//初始化容器和属性
void initSPeech();
//创建选手
void createSpeaker();
//开始比赛:控制整个流程控制函数
void startSpeech();
//抽签
void speechDraw();
//比赛
void speechContest();
//显示得分
void showScore();
//保存文件
void saveFile();
//加载文件
void loadFile();
//显示往届记录
void showRecord();
//清空记录
void clearRecord();
//判断文件是否为空
bool fileIsEmpty;
//往届记录
map<int, vector<string>>m_Record;
//析构函数声明
~SpeechManager();
//成员属性:
//保存第一轮比赛选手编号容器
vector<int>v1;
//保存第二轮比赛选手编号容器
vector<int>v2;
//显示最终胜利者容器
vector<int>vVictory;
//存放编号以及对应具体选手编号
map<int, Speaker>m_Speaker;
//存放比赛轮数
int m_Index;
};
speaker.h
//speaker.h
#pragma once
#include <iostream>
using namespace std;
class Speaker {
public:
string m_Name;
double m_Score[2];//两轮得分,创建一个数组
};
speechManager.cpp
//speechManager.cpp
#include "speechManager.h"
#define FILENAME "speech.csv"
//函数实现
//构造函数定义
SpeechManager::SpeechManager() {
//初始化容器和属性
this->initSPeech();
this->createSpeaker();
this->loadFile();
}
//展示菜单
void SpeechManager::showMenu() {
cout << "******************************************" << endl;
cout << "************* 欢迎参加演讲比赛 ************" << endl;
cout << "************* 1.开始演讲比赛 **************" << endl;
cout << "************* 2.查看往届记录 **************" << endl;
cout << "************* 3.清空比赛记录 **************" << endl;
cout << "************* 0.退出比赛程序 **************" << endl;
cout << "******************************************" << endl;
cout << endl;
}
//退出系统
void SpeechManager::exitSystem() {
cout << "欢迎下次再来:" << endl;
system("pause");
exit(0);
}
//初始化容器和属性
void SpeechManager::initSPeech() {
//容器都置空
this->v1.clear();
this->v2.clear();
this->vVictory.clear();
this->m_Speaker.clear();
this->m_Index = 1;
//将记录的容器也清空
this->m_Record.clear();
}
//创建选手
void SpeechManager::createSpeaker() {
//创建对象
string nameSeed = "ABCDEFGHIJKL";
for (int i = 0; i < nameSeed.size(); i++) {
Speaker speaker;
//将姓名初始化
speaker.m_Name = string("选手")+ nameSeed[i];
//得分初始化
for (int j = 0; j < 2; j++) {
speaker.m_Score[j] = 0.0;
}
//12名选手编号,放入v1
this->v1.push_back(i + 10001);
//将编号与演讲者插入map,以键值对方式
//map<int, Speaker>m_Speaker;已经创建map容器
this->m_Speaker.insert(make_pair(i + 10001, speaker));
}
}
// 开始比赛
void SpeechManager::startSpeech() {
// 比赛轮次循环
for (int round = 0; round < 2; round++) {
// 1、抽签
this->speechDraw();
// 2、比赛
this->speechContest();
// 3、显示结果
this->showScore();
// 准备下一轮
if (round == 0) {
this->m_Index++; // 只有从第一轮进入第二轮时需要更新索引
}
}
// 4、保存结果到文件
this->saveFile();
// 重置比赛状态,为下一届比赛做准备
this->initSPeech();//清除容器,重置轮次索引,清除当前届比赛
this->createSpeaker(); //重新创建 12 名新选手
this->loadFile();//重新加载往届记录
cout << "本届比赛完毕!" << endl;
system("pause");
system("cls");
}
//抽签
void SpeechManager::speechDraw() {
cout << "第" << m_Index << "轮选手正在抽签" << endl;
cout << "--------------------------------" << endl;
cout << "抽签后的演讲顺序如下:" << endl;
if (this->m_Index == 1) {
//第一轮比赛
random_shuffle(v1.begin(), v1.end());//将第一轮容器随机打乱
for (vector<int>::iterator it = v1.begin(); it != v1.end(); it++) {
cout << *it << " ";
}
cout << endl;
}
else {
//第二轮比赛
random_shuffle(v2.begin(), v2.end());//将第二轮容器随机打乱
for (vector<int>::iterator it = v2.begin(); it != v2.end(); it++) {
cout << *it << " ";
}
cout << endl;
}
cout << "--------------------------------" << endl;
system("pause");
cout << endl;
}
//比赛
void SpeechManager::speechContest() {
cout << "-------------第" << this->m_Index << "轮比赛正式开始--------------" << endl;
//准备临时容器存放小组成绩(降序排列)
multimap<double, int, greater<double>>groupScore;//让分数作为键进行排序,编号是值
int num = 0;//用于统计记录人员个数,6人一组
vector <int>v_Src;//比赛选手容器
if (this->m_Index==1) {
//执行第一轮
v_Src = v1;
}
else{
//执行第二轮
v_Src = v2;
}
//遍历所有的选手进行比赛
for (vector<int>::iterator it = v_Src.begin(); it != v_Src.end(); it++) {
num++;
//评委打分
deque<double>d;
for (int i = 0; i < 10; i++) {
double score = (rand() % 401 + 600) / 10.f;
//cout << score <<" ";
d.push_back(score);
}
sort(d.begin(), d.end(),greater<double>());//降序排列
d.pop_front();//去除最高
d.pop_back();//去除最低
double scoreSum = accumulate(d.begin(), d.end(),0.0f);//起始,终止,初始值
double avg = scoreSum /(double) d.size();//小数/小数
//"打印平均分"
//cout << "编号:" << *it << "\t姓名:" << this->m_Speaker[*it].m_Name
// << "\t分数:" << avg ;
//将平均分放入map容器
this->m_Speaker[*it].m_Score[this->m_Index - 1] = avg;
//将打分数据放入到临时小组容器中
groupScore.insert(make_pair(avg, *it));//均分作为键,value是具体选手编号
//每六人取出前三名
if (num % 6 == 0) {
cout << "第" << num / 6 << "小组名次:" << endl;
for (multimap<double, int, greater<double>>::iterator it = groupScore.begin(); it != groupScore.end(); it++) {
cout << "编号:" << it->second << "\t姓名:" << this->m_Speaker[it->second].m_Name
<< "\t分数:" << this->m_Speaker[it->second].m_Score[this->m_Index-1] << endl;
}
//取走前三名
int count = 0;
for (multimap<double,int , greater<double>>::iterator it = groupScore.begin(); it != groupScore.end()&&count<3; it++,count++) {
if (this->m_Index == 1) {
v2.push_back((*it).second);
}
else {
vVictory.push_back((*it).second);
}
}
groupScore.clear();//小组容器清空
}
}
cout << "-------------------第"<<this->m_Index<<"轮比赛完毕-------------------"<<endl;
system("pause");
}
//显示得分
void SpeechManager::showScore() {
cout << "---------------第" << this->m_Index << "轮晋级选手信息如下:-----------------" << endl;
vector<int>v;
if (this->m_Index == 1) {
//第一轮比赛结束
v = v2;
}
else {
v = vVictory;
}
for (vector<int>::iterator it = v.begin(); it != v.end(); it++) {
cout << "编号:" << *it << "\t姓名:" << this->m_Speaker[*it].m_Name
<< "\t分数:" << this->m_Speaker[*it].m_Score[this->m_Index-1] << endl;//不指明具体数组索引会显示数组首地址
}
cout << endl;
system("pause");
system("cls");
this->showMenu();
}
//保存文件
void SpeechManager::saveFile() {
ofstream ofs;
ofs.open(FILENAME,ios::out|ios::app);//用追加方式写文件
//将每个选手数据写入到文件中
for (vector<int>::iterator it = vVictory.begin(); it != vVictory.end(); it++) {
ofs<< *it <<","<<this->m_Speaker[*it].m_Name <<","<< this->m_Speaker[*it].m_Score[m_Index - 1]<<",";
}
ofs << endl;
ofs.close();
cout << "已经保存完成!" << endl;
//更改文件不为空的状态
this->fileIsEmpty = false;
}
//加载文件
void SpeechManager::loadFile() {
//清空m_Record中内存中的遗留数据,不是真实数据,后面会补充
this->m_Record.clear();
ifstream ifs;
ifs.open(FILENAME, ios::in);
// 1、文件不存在,自动创建
if (!ifs.is_open()) {
this->fileIsEmpty = true;
cout << "文件不存在,已自动创建" << endl;
ifs.close();
return;
}
// 2、文件存在,并且没有记录
char ch;
ifs >> ch;
if (ifs.eof()) {
cout << "文件为空" << endl;
this->fileIsEmpty = true;
ifs.close();
return;
}
// 3、文件存在,并且记录数据
this->fileIsEmpty = false;
ifs.putback(ch); // 如果读取了一个字符,将其放回输入流
string line;
int index = 0; // 记录总记录数
while (getline(ifs, line)) { // 按行读取完整数据
vector<string> v; // 存储当前行的所有字段
int pos = 0; // 搜索起始位置
int commaPos = 0; // 逗号位置
// 解析当前行的所有字段(按逗号分割)
while ((commaPos = line.find(',', pos)) != string::npos) {
v.push_back(line.substr(pos, commaPos - pos));
pos = commaPos + 1;
}
v.push_back(line.substr(pos)); // 添加最后一个字段(无逗号后的内容)
// 确保字段数量足够(至少3个:编号、姓名、分数)
if (v.size() >= 3) {
this->m_Record.insert(make_pair(index, v));
index++;
}
else {
cout << "警告:格式错误的行 - " << line << endl;
}
}
ifs.close();
////测试读取结果,以读取每一行第一个为列
//for (map<int, vector<string>>::iterator it = this->m_Record.begin(); it != this->m_Record.end(); it++) {
// cout << "第" << it->first+1 << "届记录" << endl;
// cout << "编号:" << it->second[0] << "\t名字" << it->second[1] << "\t分数" << it->second[2] << endl;
//}
}
//查看往届记录
void SpeechManager::showRecord() {
if (this->fileIsEmpty) {
cout << "文件为空或不存在!" << endl;
}
for (int i = 0; i < this->m_Record.size(); i++) {
cout << "第" << i + 1 << "届" << endl;
cout<< "冠军编号" << this->m_Record[i][0]
<< "\t名字:" << this->m_Record[i][1] << "\t分数:" << this->m_Record[i][2] << endl;
cout<< "亚军编号" << this->m_Record[i][3]
<< "\t名字:" << this->m_Record[i][4] << "\t分数:" << this->m_Record[i][5] << endl;
cout << "季军编号" << this->m_Record[i][6]
<< "\t名字:" << this->m_Record[i][7] << "\t分数:" << this->m_Record[i][8] << endl;
}
system("pause");
system("cls");
}
//清空记录
void SpeechManager::clearRecord() {
cout << "是否确定清空操作?" << endl;
cout << "1-是、2-否" << endl;
int select = 0;
cin >> select;
if (select == 1) {
ofstream ofs(FILENAME, ios::trunc);
ofs.close();
this->initSPeech();
this->createSpeaker();
this->loadFile();
cout << "清空操作" << endl;
}
else {
system("pause");
system("cls");
}
}
//析构函数定义
SpeechManager::~SpeechManager() {
}
演讲比赛管理系统.cpp
//这是演讲比赛管理系统.cpp的实现
#include "speechManager.h"
int main() {
//随机数种子
srand((unsigned int)time(NULL));
SpeechManager sm;
//sm.createSpeaker();
//for (map<int, Speaker>::iterator it = sm.m_Speaker.begin(); it != sm.m_Speaker.end(); it++) {
// cout << "编号:" << it->first
// << "\t姓名:" << it->second.m_Name
// << "\t分数:" << it->second.m_Score[0] << endl;
//}
int select = 0;
while (true) {
sm.showMenu();
cout << "请输入你的选择:" << endl;
cin >> select;
switch (select) {
case 1://开始比赛
sm.startSpeech();
break;
case 2://查看往届记录
sm.showRecord();
break;
case 3://清空比赛记录
sm.clearRecord();
break;
case 0://退出系统
sm.exitSystem();
break;
default://清屏操作
system("cls");
break;
}
}
system("pause");
return 0;
}
3、总结
这个例子主要是学会各种容器的搭配使用,涉及掌握的知识点:
1. 面向对象编程(OOP)
类与对象:定义 SpeechManager 类,封装比赛管理逻辑,包括成员函数(如 showMenu、startSpeech)和数据成员(容器、状态变量)。要掌握用一个大类去管理类的成员函数。
模块化设计:将抽签、比赛、文件操作等功能拆分为独立函数,提高代码可维护性。
2. 容器使用:
vector:存储选手编号(v1、v2、vVictory),支持动态增删。
map/multimap:m_Speaker(编号 - 选手映射)、groupScore(分数 - 编号映射,降序排序),实现快速查找和排序,这种数据解耦的思想要掌握。
deque:存储评委打分,方便去除极值(最高、最低分)。
算法应用:
random_shuffle:抽签时打乱选手顺序。
sort + accumulate:评委分数排序、求和,计算平均分。
3. 文件操作
读写模式:ios::app(追加写入,saveFile)、ios::trunc(清空文件,clearRecord)
loadfilea三种情况的编写。
文件解析:按行读取csv格式,分割逗号字段。