目录
1 演讲比赛程序要求
1.1 比赛规则
举办一场演讲比赛,共有12
人参加,比赛共2
轮,第一轮为淘汰赛,第二轮为决赛。
-
每位选手都有对应的编号:如
10001,10002...10012;A、B...、L
-
比赛方式:分组比赛,每组
6
个人比赛一场,共2
场 -
第
1
轮比赛分为2
个小组,整体按照选手编号进行抽签后顺序演讲 -
10
名评委分别给每位选手打分,去除最高分和最低分,取平均分为本轮选手的成绩 -
当第一轮比赛结束后,每个小组的前三名晋级进入下一轮比赛,后三名淘汰
-
第二轮决赛,六位选手前
3
名胜出,按成绩分为冠军亚军季军 -
每轮比赛后显示晋级选手的信息
1.2 程序功能
-
开始比赛功能:完成整届比赛的流程,比赛各个阶段需要给用户一个提示,用户按任意键后继续下一个阶段
-
查看往届记录:查看之前比赛的前三名结果,包含姓名和成绩。每次比赛都会记录到文件中,以
csv
格式保存 -
清空比赛记录:将文件中数据清空
-
退出比赛程序:可以退出当前程序
2 实现
2.1 创建管理类
功能
-
提供菜单界面与用户交互
-
对演讲比赛流程进行控制
-
与文件的读写交互
首先创建SpeachManager
的头文件和源文件
然后在头文件里实现管理类Manager
,并带上构造函数和析构函数
然后在源文件里空实现构造和析构函数,注意带作用域
2.2 菜单功能
功能:与用户交互的界面
首先在管理类中添加菜单显示接口show_menu();
然后在源文件中实现 :注意带上作用域Manager::
void Manager::show_menu()
{
cout << "******************************************" << endl;
cout << "********** 欢迎使用演讲比赛系统 **********" << endl;
cout << "********** 1、开始演讲比赛 **********" << endl;
cout << "********** 2、查看往届记录 **********" << endl;
cout << "********** 3、清空比赛记录 **********" << endl;
cout << "********** 0、退出比赛系统 **********" << endl;
cout << "******************************************" << endl;
}
然后在main
函数中调用:我们先包含SpeachManager.h
头文件,然后再根据管理类创建对象m
调用:
2.3 退出功能
功能:退出系统
目前系统只有一个显示功能,而且不支持输入与选择,因此我们使用while(true)
和switch
语句来实现该功能
在头文件里声明exit_m();
接口后在源文件中实现:
// 退出功能
void Manager::exit_m()
{
cout << "欢迎下次使用!" << endl;
exit(0);
}
修改main
函数,添加while(true)
循环和switch
语句
int main()
{
Manager m;
int input;
while (true)
{
m.show_menu();
cout << "请选择:" << endl;
cin >> input;
switch (input)
{
case 1: // 开始比赛
break;
case 2: // 显示往届记录
break;
case 3: // 清空所有记录
break;
case 0: // 退出系统
m.exit_m();
default:
system("cls"); // 清屏
break;
}
}
return 0;
}
2.4 演讲比赛功能
2.4.1 功能分析
比赛流程:
2.4.2 创建选手类
选手类speaker
,应包含属性姓名name
与分数score
(分数有2轮)
在头文件中创建:
class speaker // 选手类
{
public:
string s_name;
double s_score[2]; // 2轮得分,double使不会出现同分的情况
};
2.4.3 进行比赛
2.4.3.1 添加成员属性
在管理类中添加各种属性:
vector<int> v1; // 存放第1轮比赛的选手,共12人
vector<int> v2; // 存放第2轮比赛的选手,共6人
vector<int> v3; // 存放比赛胜出的选手,共3人
map<int, speaker> m_speaker; // 存放编号以及对应的选手 容器
int index; // 记录比赛的轮数
2.4.3.2 初始化属性
在头文件中生成初始化属性接口initSpeaker()
;在源文件中实现,并在构造函数中调用
// 初始化属性
void Manager::initSpeaker()
{
// 全部清空并置1
this->v1.clear();
this->v2.clear();
this->v3.clear();
this->m_speaker.clear();
this->index = 1;
}
调用:
2.4.3.3 创建选手
首先在管理类中添加创建选手的接口createrSperker();
然后在源文件中实现
void Manager::createSpeaker()
{
string nameSeed = "ABCDEFJHIJKL";
for (int i = 0; i < nameSeed.size(); i++)
{
string name = "选手";
name += nameSeed[i];
speaker sp; // 创建一位选手
sp.s_name = name; // 选手名字初始化
for (int j = 0; j < 2; j++)
{
sp.s_score[j] = 0; // 选手分数初始化
}
// 为选手编号
this->v1.push_back(i + 10001);
// 将选手编号以及对应选手插入到map容器中,编号10001-100012
this->m_speaker.insert(make_pair(i + 10001, sp));
}
}
然后在构造函数中调用
这样就完成了选手的创建,不过为了方便查看,我再实现一个查看map
容器中元素的函数,打印出目前选手的信息
for (map<int, speaker>::iterator it = m.m_speaker.begin(); it != m.m_speaker.end(); it++)
{
cout << "选手编号:"<<it->first<<"\t姓名:" << it->second.s_name << "\t分数:" << it->second.s_score[1] << endl;
}
2.4.3.4 开始比赛成员函数添加
首先在管理类中添加开始比赛成员函数startSpeach();
然后在源文件中空实现
// 开始比赛
void Manager::startSpeach()
{
// 第一轮比赛
// 1、抽签
// 2、比赛
// 3、显示比赛结果
// 第二轮比赛
// 1、抽签
// 2、比赛
// 3、显示比赛结果
}
2.4.3.5 抽签
抽签分为2种:第一轮抽签与第二轮抽签
我们可以根据属性index
来判断比赛进行的轮数,以给容器v1
或是v2
进行洗牌
我们先在管理类中加上抽签DrawSpeach();
接口
然后在源文件中实现
void Manager::DrawSpeach()
{
cout << "第" << this->index << "轮抽签正在进行" << endl;
cout << "***************************************" << endl;
Sleep(1000);
cout << "抽签后演讲顺序如下" << endl;
if (this->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;
}
最后在开始比赛stratSpeach
接口中调用
测试:
2.4.3.6 开始比赛
比赛流程代码可以分为4部分:
-
创建比赛的人员的容器
v_person
,并根据不同轮数将对应容器赋给v_person
-
创建人数的计数
num
,遍历v_person
里所有选手,创建deque
容器,每个选手进行10
名评委的打分并将分数插入到deque
容器,排序后去除最高分和最低分,最后获取总分和平均分,并将平均分赋值给对应轮数的score[0/1]
中 -
创建小组容器以存储对组,以平均分和编号创建对组,每
6
个选手为一组,输出这6
名选手的姓名编号与成绩 -
取出前
3
名,如果是第1
场比赛,则将前3
名插入到v2
容器,如果是第2
场比赛,则插入到v3
中,每6
个清空一次
先在管理类中创建进行比赛Speach();
接口
然后在源文件中实现
void Manager::Speach()
{
cout << "------------" << "第" << this->index << "组比赛正式开始" << "-------------" << endl;
multimap<double, int, greater<double>> Group; // 存储分数与选手编号,降序排列,放在最外面,防止进入循环创建不同的容器
// 1、创建比赛的人员的容器v_person,并根据不同轮数将对应容器赋给v_person
vector<int>v_person;
if (this->index == 1)
{
v_person = v1;
}
else
{
v_person = v2;
}
// 2、创建人数的计数num,遍历v_person里所有选手,创建deque容器,每个选手进行10名评委的打分
// 排序后去除最高分和最低分,最后获取总分和平均分
int num = 0;
for (vector<int>::iterator it = v_person.begin(); it != v_person.end(); it++)
{
// 评委打分
deque<double>d;
for (int i = 0; i < 10; i++)
{
double score = (rand() % 401 + 600) / 10.f; // 随机数600-1000,除以10后保留1位小数,是60.0-100.0
d.push_back(score); // 将分数插入到d中
}
sort(d.begin(), d.end(),greater<double>()); // 降序 排序
d.pop_back(); // 去除最低分
d.pop_front(); // 去除最高分
double sum = accumulate(d.begin(), d.end(),0); // 获取总分
double avg = sum / d.size(); // 获取均值
this->m_speaker[*it].s_score[this->index - 1] = avg; // 将均分存入到选手对应轮数的score里
// 3、创建小组容器,以平均分和编号创建对组,每6个选手为一组,输出这6名选手的姓名编号与成绩
Group.insert(make_pair(avg, *it)); // 将每位选手的均分和编号插入到Group容器中
num++; // 插入后人数++
if (num % 6 == 0) // 每插入6个人,进入一次遍历输出,此时输出的已经排序
{
cout <<"第" << num / 6 << "组选手比赛名次:" << endl;
for (multimap<double, int, greater<double>>::iterator it = Group.begin(); it != Group.end(); it++)
{
cout << "编号:" << it->second << "\t姓名:"
<< this->m_speaker[it->second].s_name << "\t平均分:"
<< this->m_speaker[it->second].s_score[this->index - 1] << endl;
}
// 4、取出前3名,如果是第1场比赛,则将前三名插入到v2容器,如果是第2场比赛,则插入到v3中,每6个清空一次
int count = 0; // 使我们可以取出前三名:0、1、2
for (multimap<double, int, greater<double>>::iterator it = Group.begin(); it != Group.end() && count < 3; it++, count++)
{
if (this->index == 1)
{
v2.push_back((*it).second);
}
else
{
v3.push_back((*it).second);
}
}
Group.clear(); // 如果不清空,则下次会有上次遍历的值
cout << endl;
}
}
cout << "--------------"<<"第" << this->index << "轮比赛结束" <<"---------------" << endl;
}
最后在开始比赛stratSpeach
接口中调用
2.4.3.7 显示比赛分数
显示比赛晋级的选手,即每轮比赛的前三名
先在管理类中添加显示比赛结果的接口函数showSpeach();
然后在源文件中实现
void Manager::showSpeach()
{
cout << "------------" << "第" << this->index << "轮选手晋级信息:" << "-------------" << endl;
vector<int>v;
if (this->index == 1)
{
v = v2;
}
else
{
v = v3;
}
for (vector<int>::iterator it = v.begin(); it != v.end(); it++)
{
cout << "编号:" << *it << "\t姓名:"
<< this->m_speaker[*it].s_name << "\t平均分:"
<< this->m_speaker[*it].s_score[this->index - 1] << endl;
}
cout << endl;
system("pause");
system("cls");
}
最后在开始比赛stratSpeach
接口中调用
2.4.3.8 第二轮比赛
把第一轮比赛流程的代码加到第二轮即可
不过注意场次index
要++
2.4.4 保存分数
先在管理类中添加保存比赛结果的接口函数saveSpeach();
然后在源文件中实现
void Manager::saveSpeach()
{
ofstream ofs;
ofs.open("Speach.csv", ios::out | ios::app);
// 以输出和追加方式打开文件,后缀命名为csv
// 将最终获胜的3人数据写入文件
for (vector<int>::iterator it = v3.begin(); it != v3.end(); it++)
{
ofs << *it <<"," <<this->m_speaker[*it].s_score[1] << ",";
}
ofs << endl;
ofs.close();
cout << "文件已保存!" << endl;
}
2.5 查看比赛记录
2.5.1 读取记录分数
先在管理类中添加读取记录分数的接口函数loadSpeach();
以及记录文件是否为空的标识符fileISempty
和存放往届记录的容器map<int,vector<string>> s_Load
然后在源文件中实现:
读取文件时,会有3种情况:
-
Ⅰ文件不存在
ifstream ifs("Speach.csv", 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;
}
-
Ⅲ 文件不为空
因为不同数据之间用逗号, 隔开,我们通过substr
截取开头到逗号,中间的数据来分割每个数据
而下一个截取的开头就是上次截取结尾+1
,结尾就是下一个逗号,
的位置
然后将每个数据存放到一个vector
容器中,最后将容器以对组插入到map
容器中
// 3、文件不为空
this->fileISempty = false;
ifs.putback(ch);// 将上面截取的1个字符放回
string data; // 存储文件中读取到的信息
int session = 1; // 比赛的届数
while (ifs >> data)
{
vector<string>v; // 存储选手编号以及分数的容器,直接都用string类型
int pos = -1; // 查到,逗号的位置,将位置存储在pos
int start = 0;// 起始位置
while (true)
{
pos = data.find(",", start);// 从start 0开始查找 ,逗号
// 10003,80.375,10004,80,10007,79.375
// start 1 pos ,逗号所在的位置,中间要截取的就是pos-start
if (pos == -1)
{
// 没找到
break;
}
string tmp = data.substr(start, pos - start);
v.push_back(tmp);
start = pos + 1; // 让start移到pos+1的位置
}
this->s_Load.insert(make_pair(session, v)); // 将届数和选手信息插入到s_Load里
session++;
}
2.5.2 查看记录功能
先在管理类中添加查看记录分数的接口函数showRecord();
然后在源文件中实现:判断完文件不为空后,直接遍历输出即可
void Manager::showRecord()
{
if (this->fileISempty)
{
cout << "文件为空或不存在!" << endl;
}
else
{
for (map<int, vector<string>>::iterator it = s_Load.begin(); it != s_Load.end(); it++)
{
cout << "第" << it->first << "届 "
<< "\t冠军编号为:" << it->second[0] << "\t分数为:" << it->second[1] << endl
<< "\t亚军编号为:" << it->second[2] << "\t分数为:" << it->second[3] << endl
<< "\t季军编号为:" << it->second[4] << "\t分数为:" << it->second[5] << endl;
Sleep(40);
}
}
Sleep(500);
system("pause");
system("cls");
}
2.5.3 bug解决
-
Ⅰ当
Speach.csv
中没有数据时,我们进行一场比赛,结束后数据会存放到文件中,但这时直接运行2
查看记录分数时仍然会提示文件为空
进行比赛
再次使用2
,仍然为空
解决:我们每次保存文件时,都应在最后使fileISempty
为false
这样就解决了
-
Ⅱ不过就上面解决后,我们继续执行代码,重复操作
会发现程序没有任何输出,这是因为数据没有实时更新
解决:我们需要再次初始化属性、创建选手并获取往届记录
即将构造函数中的函数在进行比赛的接口Speach();
中调用
-
Ⅲ 初始化时,没有初始化记录容器
s_Load
会导致我们怎么清空容器,容器都存在数据
解决:在初始化接口initSpeaker();
中初始化记录容器
2.6 清空功能
先在管理类中添加清空文件的接口函数destroyRecord();
然后在源文件中实现
void Manager:: destoryRecord()
{
cout << "是否确认清空?" << endl;
cout << "确认:1" << endl;
cout << "取消:0" << endl;
cout << "请输入:" << endl;
int input;
cin >> input;
if (input == 1)
{
ofstream ofs("Speach.csv", ios::trunc);
// trunc:如果文件存在,删除并重新创建
ofs.close();
// 然后将属性初始化
this->initSpeaker(); // 初始化
this->createSpeaker(); // 创建选手
this->loadSpeach(); // 获取往届记录
cout << "清空成功!" << endl;
}
system("pause");
system("cls");
}
最后在main
函数中调用
测试:
至此该项目全部完成