简介:一份面向数据结构课程设计的实验报告,围绕运动会分数统计问题展开,适合计算机相关专业学生完成链表、结构体与综合编程练习时参考。报告完整呈现了从需求分析到编码实现的流程:采用线性链表存储运动员与学院数据,定义结构体封装参赛项目、姓名、性别、学院编号、名次及得分;程序按前五名7、5、3、2、1的计分规则自动生成学院团体总分报表。实现中重点展示了初始化、输入、算分、输出四个函数的设计,并对性别和学院编号等输入进行健壮性校验,采用递归方式处理错误重输。附录附有可直接运行的C++源代码,另含测试用例设置、链表指针操作与初始化等调试分析,便于对照运行和排错。资源为单个DOCX文档,全文约91KB,内容紧凑,已有391人学习下载,适合正在攻克链表操作、结构体应用及输入校验等知识点的学习者借鉴。
1. 运动会分数统计这套 C++ 代码:先搞懂双表结构再动手
「运动会分数统计」几乎是数据结构课程设计里出场率最高的题目:一个学院列表、一张比赛成绩明细、一套前五名计分规则,看着简单,真动手写起来,输入校验、男团女团拆分、总分汇总,每一处都有地方能让你翻车。这份实验报告加配套源码的价值在于:它用学生表和学院表两张线性表把「明细记录」和「汇总报表」分开建模,在 n≤20、m≤30、w≤20 的典型规模下思路很直接,错误处理也做得很足——性别不合法、项目号越界、名次超范围都会当场拦住重输。适合正在做类似学院成绩统计作业的人,也适合想看看 C++ 结构体数组到底怎么组织数据的从业者。我拆完之后,把四个核心函数、参数传递方式和几个隐蔽 bug 都标了出来。
2. 双线性表存储:结构体数组当线性表用,答辩时别说是链表
2.1 学生表与学院表的分工:明细数据与汇总数据分开存
这道题的数据分成两类:一类是每位运动员的参赛明细,一类是每个学院的汇总得分。报告里给的第一张表是 student,存的是「一行一条参赛记录」:参赛项目号、姓名、性别、学院编号、名次、得分。注意这里说的「一行一条参赛记录」,不是「一个运动员一条」,因为同一个学院的运动员可能参加不同项目,同一个运动员也可能报多个项目,所以明细表是按记录追加的。
第二张表是 college,存的是「一行一个学院」:学院编号、男子团体总分、女子团体总分、总成绩。两张表通过 collegenum 关联,而不是通过姓名关联,这个设计很务实——学院编号是稳定主键,姓名可以重复,B 站上叫「张三」的一抓一大把。
这样拆的好处是什么?算分阶段只需要遍历学院表,对每个学院回过头扫一遍学生表,把性别为 b 的得分累进 Tboy,性别为 g 的得分累进 Tgirl,最后 Tall = Tgirl + Tboy。输出阶段也一样,按学院号分组筛明细。如果只建一张表硬算,每次输出都要重复扫全表,代码会乱。
2.2 结构体数组的取舍:报告写的是线性链表,代码其实是顺序表
这里有个答辩时很容易被老师抓的点:实验报告的文字部分写的是「使用线性链表」,但源码里 student 和 college 两个结构体都没有 next 指针,也没有 malloc 动态分配节点,实际上用的是结构体数组加上一个 last 游标来模拟线性表。
student 里 last 记录的是当前已存入的记录下标,初始设为 -1,每成功录入一条就 last++;college 里 last 初始化为 n-1,表示一共有 n 个学院。这是典型的「数组 + 游标」顺序表实现,优点是随机访问快、不用管指针悬空,缺点是 MAXSIZE 固定为 100,超过就拒绝录入。
题目规模 n≤20,m≤30,w≤20,单个项目的参赛人数不可能无限膨胀,100 条记录足够。真要说链表,这里的「线性表」指的是逻辑层面的线性关系,存储层面用数组没任何问题。但我给你的建议是:答辩时主动说清楚「我用的是结构体数组实现的顺序表,因为本题规模固定且需要频繁按下标访问,数组比链表更合适」,比被动被老师问「你链表指针在哪」要稳得多。
2.3 item、sex、point 这些字段的映射关系,决定了校验怎么写
看代码的时候,最容易晕的是项目号和性别的联动关系。题目规定:项目编号 1~m 是男子项目,m+1~m+w 是女子项目。也就是说,项目号本身携带了性别信息,输入时必须校验「项目号落在男子区间但性别填了 g」这类矛盾数据。
源码里的校验逻辑是这样写的:
if(S1->item[i]>=0&&S1->item[i]<=m&&S1->sex[i]!='b'){ cout<<"您的输入有误,项目号与性别不匹配,请重新输入该项目!"<<endl; goto a; } if(S1->item[i]>=m+1&&S1->item[i]<=m+w&&S1->sex[i]!='g'){ cout<<"您的输入有误,项目号与性别不匹配,请重新输入该项目!"<<endl; goto a; }第一段判断的是:项目号落在 0~m 之间,但性别不是 b,就报错。这里有个细节,判断区间从 0 开始,而合法项目号最小是 1。不过没关系,因为在这段代码之前已经拦了一道S1->item[i]==0的检查,item 为 0 的输入早在性别匹配之前就被拒掉了,所以这个「多出来」的 0 不会产生漏网之鱼,但如果你要自己改造这段代码,建议把区间起点改成 1,逻辑更干净。
第二段判断的是女子项目区间 m+1~m+w,性别不是 g 就报错。性别字符用的是 b(boy)和 g(girl),不是常见的 M/F,输入提示里也写清楚了。这些字段的语义关系捋顺之后,再看输入函数和算分函数就不会觉得校验顺序奇怪了。
3. 核心函数逐段拆解:初始化、输入、算分、输出的参数怎么传
3.1 InitSCH:学院表为什么要传指针
void InitSCH(college *S,int n){ int i; S->last=n-1; for(i=0;i<S->last+1;i++){ S->collegenum[i]=i+1; S->Tgirl[i]=0; S->Tboy[i]=0; S->Tall[i]=0; } }这个函数做的事很单纯:把学院表的下标 i 位置写成学院号 i+1,三个总分字段全部清零,last 设为 n-1。main 里调用方式是InitSCH(&S2,n),因为函数内部要修改的是实参 S2 的字段值,如果按值传参,函数里改的只是一份拷贝,调用结束后全部丢失。这里用的是二级指针吗?不是,S2 本身是结构体变量,取地址后传college*,一级指针就够。
参数 n 是学院总数,它也决定了 college 数组里有效数据的长度。这里有个隐藏约定:college 数组的每个下标 slot 和学院号是一一对应的,collegenum[i] 永远等于 i+1,所以后面 mark 累加时可以直接用L2->collegenum[i]==L1->collegenum[j]匹配,不需要额外查表。
3.2 input:递归重输是怎么实现的,# 号结束符怎么收尾
input 是这份代码里最长的函数,也集中了最多的健壮性判断。它的整体流程是:先判断学生表是不是满了,满了直接返回 RIGHT;然后进入一个用 goto 标记的输入循环,提示用户输入一行数据;读到 # 就跳到结束标签 b 收尾;否则把五个字段分别存入结构体的对应数组位置,然后做一串校验,任何一项不过就 goto 回标签 a 重新输入。
核心代码简化后是这样:
int input(student *S1,int n,int m,int w){ string a1,a2,a3,a4,a5,a6; int i=0; a: cout<<"项目号/姓名/性别(b男,g女)/学院号/名次,结束请按#"<<endl; cin>>a6; if(a6=="#") goto b; a1=a6; cin>>a2>>a3>>a4>>a5; S1->item[i]=str_int(&a1[0]); S1->studentname[i]=a2; S1->sex[i]=a3[0]; S1->collegenum[i]=str_int(&a4[0]); S1->num[i]=str_int(&a5[0]); // 一串 if 校验,失败 goto a i++; S1->last=i; goto a; b: S1->last=i; return RIGHT; }这里的参数传的是student *S1,不是引用。报告的文字部分写的是「必须用指针(引用)类型」,这句话说得有点含糊,准确说是:因为要带回数据,必须传地址。C 风格用指针,C++ 风格也可以用引用,两种写法都能实现「函数内修改实参」的效果。源码里用的是指针,main 调用时传&S1。
str_int 函数是把字符串转成整数,实现方式是逐位扫描字符,遇到非数字就停。它只处理正整数场景,项目号、学院号、名次全是正整数,所以够用。goto 重输的机制是把整个五个字段全部重新读一遍,不是只重读出错的那一个字段,这种「整行重来」的做法在实际命令行工具里也很常见,省得维护「哪个字段错了」的现场状态。
3.3 mark:双重循环累加男团女团总分
mark 函数是整份代码的算分核心,名字取得很直白。它分两个阶段:第一阶段给每条运动员记录算个人得分,第二阶段按学院累加。
void mark(student *L1,college *L2,int n,int m,int w){ int i,j; for(i=0;i<L1->last+1;i++){ if(L1->num[i]==1) L1->point[i]=7; if(L1->num[i]==2) L1->point[i]=5; if(L1->num[i]==3) L1->point[i]=3; if(L1->num[i]==4) L1->point[i]=2; if(L1->num[i]==5) L1->point[i]=1; if(L1->num[i]>5) L1->point[i]=0; } for(i=0;i<L2->last+1;i++){ for(j=0;j<L1->last+1;j++){ if(L2->collegenum[i]==L1->collegenum[j]){ if(L1->sex[j]=='b'){ L2->Tboy[i]+=L1->point[j]; } if(L1->sex[j]=='g'){ L2->Tgirl[i]+=L1->point[j]; } } } L2->Tall[i]=L2->Tgirl[i]+L2->Tboy[i]; } }参数 n、m、w 在这个函数里其实没有直接被使用,但保留它们让接口语义完整:n 是学院数,决定外层循环的次数范围(实际上用的是 L2->last),m 和 w 是项目数,决定性别校验的区间。这里看起来是「冗余参数」,但如果你在后面扩展需求,比如按项目类别分开排名,这三个参数就用得上了。
名次和得分的映射表是 7、5、3、2、1,写在连续 if 里。0 分的情况有两个来源:名次大于 5,或者名次小于等于 0——后者在 input 阶段已经被拦截了,所以走到这里能处理的基本只有名次 6 及以上。累加阶段是双重循环,外层学院,内层运动员,时间复杂度 O(n×p),p 是参赛记录数。n≤20、p<=100 的规模下,这个复杂度毫无压力,没必要上哈希表。
3.4 output:按学院分组打印成绩单
void output(student L1,college L2){ int i,j; for(i=0;i<L2.last+1;i++){ cout<<"第"<<i+1<<"号学院的成绩"<<endl; cout<<"学院\t项目\t姓名\t性别\t名次\t得分"<<endl; for(j=0;j<L1.last+1;j++){ if(L2.collegenum[i]==L1.collegenum[j]){ cout<<L1.collegenum[j]<<'\t'<<L1.item[j]<<'\t' <<L1.studentname[j]<<'\t'<<L1.sex[j]<<'\t' <<L1.num[j]<<'\t'<<L1.point[j]<<endl; } } cout<<i+1<<"号学院的女子团体总分为:"<<L2.Tgirl[i]<<endl; cout<<i+1<<"号学院的男子团体总分为:"<<L2.Tboy[i]<<endl; cout<<i+1<<"号学院的全体运动员总分为:"<<L2.Tall[i]<<endl; cout<<endl; } }这里有个 C++ 的参数传递细节容易被忽略:input 和 mark 传的是指针,output 传的却是值。原因很简单——output 只读数据,不修改任何字段,按值传参会复制整个结构体。student 结构体里光是 studentname 数组就有 100×20 个字符,整个结构体复制一遍开销不小。不过这个规模下无所谓,但如果你想把代码改得更规范,output 也改成传const student&,既能只读又省拷贝,这才是现代 C++ 的写法。
4. 用 n=4、m=3、w=2 试跑:一份可复现的测试输入和验证方法
4.1 输入格式说明:项目号/姓名/性别/学院号/名次,用空格分隔
程序启动后会先问你三个参数:学院个数、男子项目数、女子项目数。测试题目给了推荐值 n=4,m=3,w=2。这里我多说一句:程序用 string 接收数字再转 int,所以你输入4会被 str_int 转成整数 4,输入4abc也会被转成 4 而不会报错——这个坑后面避坑章细讲。
进入 input 后,每条记录占一行,五个字段用空格隔开,顺序是:项目号 姓名 性别 学院号 名次。项目号的取值范围受 m、w 约束:本项目里 1~3 是男子项目,4~5 是女子项目。性别只有 b 和 g 两个合法值。
4.2 测试数据示例:覆盖正常、学院号越界、性别不匹配三种情况
建议按下面这张表录入,六条数据覆盖了正常数据、非法学院号、性别不匹配和名次超范围四类场景:
| 序号 | 项目号 | 姓名 | 性别 | 学院号 | 名次 | 预期结果 |
|---|---|---|---|---|---|---|
| 1 | 1 | ZhangSan | b | 1 | 1 | 接受,得 7 分 |
| 2 | 1 | LiSi | b | 2 | 2 | 接受,得 5 分 |
| 3 | 2 | WangWu | b | 1 | 3 | 接受,得 3 分 |
| 4 | 4 | ZhaoLiu | g | 1 | 1 | 接受,得 7 分 |
| 5 | 3 | QianQi | g | 1 | 1 | 拒绝:项目号与性别不匹配 |
| 6 | 1 | SunBa | b | 5 | 1 | 拒绝:学院号不能超过 4 |
注意第二条和第三条,同一学院 1 号在项目 1 和项目 2 都有得分,这是为了验证累加逻辑。如果你要额外验证边界,可以录入一条学院号为 0 的数据,测试前两个校验分支的先后顺序。
4.3 从输出反推算分逻辑:逐行核对明细单
程序会按学院分组输出成绩单。对正常录入的四条数据,预期输出应该是这样:
学院 1 的成绩单里应有两条明细:项目 1 第 1 名 7 分,项目 2 第 3 名 3 分,项目 4 第 1 名 7 分,女团总分 7,男团总分 10,总分 17。学院 2 只有一条:项目 1 第 2 名 5 分,男团 5,女团 0,总分 5。学院 3 和学院 4 无参赛记录,三行总分全为 0,但成绩单依然会打印出来。
这个输出行为本身是一个值得注意的设计决策:即使某个学院没有任何运动员参赛,也会输出一张空成绩单,方便确认「数据没问题,只是没参赛」。如果你在做类似的工程,这种「保证每个学院都出现在报表里」的做法值得沿用。
5. 避坑指南:名次校验、goto 重输和 str_int 的隐蔽问题
5.1 名次范围写成 <= n:学院数小于 5 时第 4、5 名被误拒
现象:设 n=4,m=3,w=2 跑测试,录入一条名次为 4 的合法数据,程序直接报错「输入名次>0 且<=4」。
原因:源码里校验写的是S1->num[i]<=n,它把名次上限错误地绑定到了学院数 n 上。但名次规则是取前五名,得分顺序 7、5、3、2、1,名次上限应该恒为 5,跟学院数没有任何关系。当 n=4 时,名次 4 会被正常接受,名次 5 会被误拒;当 n=3 时,名次 4 和 5 都会误拒。这个 bug 在题目给的标准测试规模 n=4 下,恰好挡住了第 5 名。
解决:把条件改成S1->num[i]>0 && S1->num[i]<=5。更稳妥的做法是从计分规则常量反推,比如定义#define TOP_N 5,校验和 mark 里的得分映射都引用同一个常量,避免两处不一致。
5.2 性别校验必须用 &&:用 || 会放行所有非法性别
现象:把性别判断写成if(S1->sex[i]!='g'||S1->sex[i]!='b'),然后输入任意字符,程序都不报错,全当合法数据处理。
原因:逻辑短路。当输入不是 b 也不是 g 时,第一个条件成立直接进入分支;当输入是 g 时,第二个条件!='b'成立也进入分支;当输入是 b 时,第一个条件!='g'成立还是进入分支。用 || 连接,恒真,等于没校验。这份报告在调试分析里特别写了这一点,说明作者实际踩过这个坑。
解决:必须用 &&,S1->sex[i]!='g'&&S1->sex[i]!='b',两个条件同时成立才说明性别既不是 b 也不是 g。这个属于逻辑运算符语义的基本功,但确实是最容易手滑写错的地方,建议测试时专门输入一个x验证。
5.3 str_int 只检查首字符:12a 会被当成 12,数据清洁度不可依赖它
现象:录入项目号位置输入12a,学院号输入3xyz,程序照收不误,算分结果跟输入 12 和 3 完全一样。
原因:str_int 的实现从字符串首字符开始累加,while(str[i] && isdigit(str[i]))遇到非数字立即停止。它只保证首字符必须是数字,对后续字符没有约束。input 里的校验也只看了a1[0]>=48&&a1[0]<=57,查的是首字符,所以12a能通过第一道检查,然后被 str_int 解析成 12。
解决:两个方案,一是把 input 里的校验改成全串遍历,确认每个字符都是数字;二是直接用atoi或stoi并捕获异常。实用角度,建议在这个题目范围内保持简单:校验函数改成遍历a1的每个字符,遇到非数字就报错。不要过度设计,但至少要保证「非法输入在入口处被拦死」。
5.4 输入中段想结束做不到:只有项目号位置输 # 才触发收尾
现象:录到一半想提前结束,在姓名位置输#,程序把#当成了姓名,然后继续等学院号和名次,最后报「学院号不能为非数字」。
原因:input 的收尾判断if(a6=="#")只发生在每次循环的最开头,也就是读项目号的位置。一旦读入正常项目号,后面四个字段任意位置输 # 都不会再触发收尾。这是这个交互设计的天然限制。
解决:不改逻辑也行,但你要在输入提示里明确写「结束请在项目号位置输 #」。如果想把体验做好,可以在每读一个字段后都判断一次#,遇到就回退并结束输入。考虑到这只是一份课程设计代码,不改也完全能交差,只要文档说明清楚即可。
5.5 提示文案里的斜杠会误导输入格式:实际是空格分隔
现象:程序提示「项目号(数字)/姓名(字符串)/性别(b:男,g:女)/学院号(数字)/名次(数字)」,照着提示用斜杠分隔输入,第一关项目号校验就报「非数字」。
原因:cout 里的/只是给人看的分隔符示意,真正控制输入格式的是cin>>a1>>a2>>a3>>a4>>a5,它按空白字符分隔,空格、Tab、换行都行,唯独斜杠不行。项目号位读到/时,a1[0]==47,不在 48~57 之间,直接被拦。
解决:按空格分隔输入即可。如果你要改提示,把/换成空格或逗号,减少误解。这条属于「提示与行为不一致」的典型问题,放到工程里就是文档和代码不同步,会让用户摸不着头脑。
6. 验证技巧:用手工算分对照表校核 mark 函数
6.1 造一组能算清账的临界数据
mark 函数是黑匣子,你不能只靠肉眼扫一遍双重循环就确认它对。我的习惯是先造一组「手算结果显而易见」的数据,再对照程序输出逐行核对。建议用这组:
| 序号 | 项目号 | 姓名 | 性别 | 学院号 | 名次 |
|---|---|---|---|---|---|
| 1 | 1 | ZhangSan | b | 1 | 1 |
| 2 | 1 | LiSi | b | 2 | 2 |
| 3 | 4 | WangWu | g | 1 | 1 |
| 4 | 4 | ZhaoLiu | g | 3 | 3 |
这里特意选了男子项目 1 和女子项目 4,覆盖 b/g 两种性别;学院 1 同时有男有女,学院 2 只有男,学院 3 只有女,学院 4 空。
6.2 手算结果表
根据 7、5、3、2、1 的计分规则,手工算出来的学院汇总如下:
| 学院号 | 男子团体总分 | 女子团体总分 | 团体总分 |
|---|---|---|---|
| 1 | 7 | 7 | 14 |
| 2 | 5 | 0 | 5 |
| 3 | 0 | 3 | 3 |
| 4 | 0 | 0 | 0 |
这张表就是你验证程序正确性的基准。算的时候留意一个点:学院 4 没有任何参赛记录,但它的总分必须显示 0 而不是留空,这能顺带验证 InitSCH 的清零逻辑有没有生效。
6.3 把程序输出和手算结果逐项对齐
跑程序把这四条数据录进去,重点核对三处:学院 1 的明细单里是否恰好出现两条男子项目记录和一条女子项目记录;每个学院输出末尾的三行总分是否和上表完全一致;另外再故意输两条错误数据(如学院号 5、性别 x),确认程序会拦住重输。这一步的意义在于:错误处理代码写得再漂亮,不实际触发一次,你永远不知道它拦的是不是你想拦的那条。
从那以后我每次改这类统计程序,都会先在纸上把期望结果写完,再上机跑数据逐行对齐,发现不一致先怀疑自己的算分函数,再怀疑输入数据。这套习惯也让我在这个题目里少走了很多弯路,希望帮到你。
本文还有配套的精品资源,点击获取