C++顺序表实现运动会分数统计:从课程设计到边界修复
简介一份面向数据结构课程设计的运动会分数统计实验报告围绕线性链表和结构体完成比赛成绩的录入、算分与报表输出。报告详细展示了运动员和学院两类线性表的定义方式给出初始化、输入、算分、输出等核心函数的设计思路针对性别校验、非法项目号等边界情况提供了错误处理与递归重输方案并补充了调试分析与代码优化反思。附带的C源程序可帮助读者快速理解链表节点操作、结构体字段初始化和健壮性检查的实际应用。资源包为单个docx文件大小91KB适用于正在完成类似课设、需要参考程序结构与排错思路的高校学生。已有389人浏览学习内容覆盖实验目的、要求、数据定义、函数实现、运行测试、调试分析和源程序附录能够作为课程报告撰写与程序修改的直接参考。1. 一份 2010 年的课程设计运动会分数统计到底在统计什么这份实验报告解决的是一个边界非常清楚的统计问题n 个学院参加运动会男子项目编号 1m女子项目编号 m1mw每个项目取前五名得分固定为 7、5、3、2、1。程序输出两种报表按学院展开的成绩单和含学院号、男子总分、女子总分、团体总分的汇总表。代码用 C 实现核心是两张用结构体数组模拟的顺序表外加初始化、输入、算分、输出四个函数。这类题在互联网课程设计资源里很常见参数规整n≤20、m≤30、w≤20测试固定为 n4、m3、w2。适合正在做课程设计的学生也适合想复习 C 风格顺序表、研究校验与累加逻辑的工程师。下面按数据模型、输入校验、算分输出三条线拆解最后给出几处必须修的边界问题。2. 结构体数组模拟线性表数据模型、last 下标与初始化函数报告正文里写的是线性链表但源码实际用的是定长结构体数组加一个 last 下标本质是顺序表。名称写错在学生报告里很常见但这个设计意图值得说清楚当前场景根本没有用到链表的动态插入和删除用数组模拟顺序表才是合理选择。2.1 为什么顺序表够用链表反而添乱先算数据量上限。所有项目都取前五名最多录入 (mw)×5 条运动员记录。按题目给的上限 m30、w20 算是 50 个项目乘 5 人共 250 条。操作只有两种输入时在表尾追加一条输出时从头到尾遍历并按学院号过滤。中间插入、按名次删除这类链表拿手操作一次都不会发生。顺序表的优势在这个场景下正好命中数组连续内存遍历时缓存友好按下标访问是 O(1)表尾追加只维护一个 last。链表则要为每个节点分配内存、维护指针遍历时反而多一次寻址。所以报告里线性链表是术语误用代码里用数组实现顺序表是对的。报告提到的指针使用过程中很容易出错指的应该是函数传参时的指针而不是链表节点的指针。一个需要留意的容量问题是 MAXSIZE 定为 100而 (mw)×5 最大是 250。当数据量超过 99 条时input 函数里S1-last MAXSIZE - 1的判断会直接拒绝继续录入。如果严格按题目给的 n、m、w 上限跑这个容量是不够的后面第 5 章会说怎么改。2.2 两张表的结构体定义与字段语义#define MAXSIZE 100 typedef struct { int item[MAXSIZE]; // 参赛项目号 string studentname[MAXSIZE]; // 运动员姓名 char sex[MAXSIZE]; // 性别b 男g 女 int collegenum[MAXSIZE]; // 所在学院编号 int num[MAXSIZE]; // 名次 int point[MAXSIZE]; // 得分 int last; // 最后一条记录的下标 } student; typedef struct { int collegenum[MAXSIZE]; // 学院编号 int Tgirl[MAXSIZE]; // 女子团体总分 int Tboy[MAXSIZE]; // 男子团体总分 int Tall[MAXSIZE]; // 团体总分 int last; } college;各字段的含义和约束整理如下字段类型含义取值约束itemint参赛项目号1~m 为男子项目m1~mw 为女子项目studentnamestring运动员姓名长度不超过 20 个字符sexchar性别b 男g 女collegenumint所在学院编号1~nnumint名次前五名有效pointint得分7 / 5 / 3 / 2 / 1无效名次为 0lastint最后一条记录的下标空表为 -1这种写法叫数组结构体Structure of Arrays每条运动员记录被拆成 6 个平行数组同一下标拼起来才是一条完整记录。好处是内存连续、下标访问直接坏处是拷贝、排序、删除任何一条记录都要同步维护所有数组的下标漏一个就错位。相比之下用结构体数组Array of Structures内聚性更强一条记录就是一个对象只动一个元素。老代码用 SoA 多半是为了省内存或配合固定列宽输出课程设计里用结构体数组写起来更不容易出逻辑错误。2.3 InitSCH 与 last 的边界约定void InitSCH(college *S, int n) { int i; S-last n - 1; // n 个学院占用下标 0~n-1 for (i 0; i S-last 1; i) { S-collegenum[i] i 1; // 学院编号从 1 开始连续分配 S-Tgirl[i] 0; // 三个总分必须清零否则累加会混入随机值 S-Tboy[i] 0; S-Tall[i] 0; } }这里 last 存的是最大下标而不是元素个数。n 个学院占用下标 0 到 n-1所以 last n-1循环条件i S-last 1等价于i n。这个约定要和主函数里对 student 表的处理保持一致——main 里把S1.last初始化为 -1 表示空表后面 input、mark、output 遍历时统一用i last或i last 1。三组分数清零是必须的。C 语言局部变量不初始化时是随机值累加前不归零总分里会混进垃圾数据。InitSCH 存在的意义就是把学院编号连续、分数从 0 开始这两个不变式一次性建立起来。传指针的原因也和 last 有关函数内部要写S-last如果不传地址main 里的 S2 拿不到更新后的 last。这也是报告里反复强调输入函数必须用指针传参的原因。提示collegenum 与下标存在值等于 i1的对应关系。下面算分时会反复利用这个关系如果以后改成乱序编号双重循环的匹配条件不用改但 InitSCH 的赋值逻辑要动。3. input 输入函数拆解goto 循环、str_int 与逐字段校验input 是整个程序里最长的函数也是错误处理最密集的地方。它要做的事拆开看是三块读取一条记录、逐字段校验、失败时回到起点重读。实现上用了 goto 标签和手写字符串转换函数这两处是阅读老代码时最容易卡住的地方。3.1 输入协议与 # 结束符输入格式是每行六段用空格分隔项目号、姓名、性别、学院号、名次。性别用单个字符 b男或 g女。以 # 作为整批输入的结束标记。a: cout 项目号/姓名/性别/学院号/名次 endl; cout 若要结束输入请按# 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]); // 逐项校验任一失败 goto a重新读当前这一条 i; S1-last i; goto a; b: S1-last i; return RIGHT;这里有个取巧的设计先用 string 变量 a6 读第一个词判断是不是 #如果不是再把 a6 赋给 a1 当作项目号继续解析。这样结束判断和正常读取共用一次读操作少写一个分支。代价是项目号也得走字符串转换为后面 str_int 的出现埋下伏笔。注意失败时 i 没有自增所以 goto a 之后新数据会覆盖当前下标的旧数据不会产生空洞记录。3.2 str_int 的实现与边界条件int str_int(char *str) { int num, i; num *str - 0; // 首字符 ASCII 码减 0 得到第一位数字 i 1; while (str[i] isdigit(str[i])) num num * 10 str[i] - 0; // 后续数字逐位乘 10 累加 return num; }逻辑是第一个字符的 ASCII 码减掉 0 得到第一位数字然后从第二个字符开始只要还是数字字符就乘 10 累加。题目给的数据范围里项目号、学院号、名次最多两位数字这个函数够用。但它有三个很明确的假设第一个字符必须是数字没有处理负数、前导空格和空串遇到 1a 这种混合串解析到 a 就停返回 1而调用方只检查了首字符是数字所以 1a 会被当成 1 收下。在课程设计里可以接受但如果数据来源变成文件或网络接口这个函数必须换成stringstream或std::stoi并做整体校验。3.3 校验规则顺序、边界与两个隐藏漏洞代码把校验写成了一个很长的 while(1) 块每条规则失败都用 goto a 跳回输入起点。逐条整理如下校验项判定条件失败处理项目号是数字a1[0] 不在 0~9 范围goto a 重输项目号非 0item[i] 0goto a 重输学院号是数字且 ≤ na4[0] 非数字或 collegenum[i] ngoto a 重输性别合法sex[i] 不是 b 也不是 ggoto a 重输项目与性别匹配项目在 1~m 但性别不是 b项目在 m1~mw 但性别不是 ggoto a 重输名次范围num[i] ≤ 0 或 num[i] ngoto a 重输两个隐蔽问题值得单独说。第一项目号只查了非 0和首字符是数字没有查上限item[i] m w。输入项目号 99、性别 b、学院号 1会顺利通过所有匹配检查——因为 99 既不在 1~m也不在 m1~mw两个不匹配判断都是假数据就被收下了。算分时它不影响任何学院总分但会作为脏数据出现在成绩单里。第二名次上限用的是 n学院数而不是 5。测试用例 n4 时真正的第五名会被num[i] n拒绝录不进去反过来 n6 时名次 6 会被收下然后在算分时得 0 分。正确做法是名次范围取 1~5与取前五名规则保持一致。报告里还提到一个细节判断性别是否合法时S1-sex[i] ! g和S1-sex[i] ! b必须用并且关系。如果误写成或那任意性别都会同时满足两个条件之一判断永远成立等于没校验。这类逻辑在写条件判断时特别容易反建议在注释里把非男且非女才报错写明白。3.4 递归重输与 goto两种失败重试方案报告的调试分析里写了用递归调用的方法实现重新输入但最终源码用的是 goto。这两种方案的差别值得说清楚。递归方案是在检测到错误后在 input 函数内部再调用一次 input。它能工作但有两个问题一是每次错误都加深一层调用栈连续错几次虽然不至于栈溢出但行为不可控二是重新输入的语义不好定义——是整批重来还是只重读当前记录递归里不好表达。goto 方案把范围缩得很小标签 a 到 goto a 之间的代码就是读取并校验一条记录失败只重读当前这条之前成功录入的数据不受影响。可读性差是 goto 的代价但在这个单函数循环里它的行为最容易验证。实际工程里我会用 while 加状态标志改写bool valid false; while (!valid) { valid true; cin token; if (token #) break; // 解析字段写入当前下标 // 任一校验失败 - valid false; continue; }语义和 goto 完全一致但避免了标签跳转后续也好加连续失败次数上限这类保护。这里要收紧的就是 3.3 里指出的名次上限和项目号上限两处测试时用 n4、m3、w2 正好能暴露前面的名次问题。4. mark 算分与团体总分报表双重循环累加与 n4 测试算分和输出是连在一起的mark 先给每条运动员记录算出得分再把得分按学院和性别累加output 再按学院维度把成绩单和总分打出来。这两步分开职责更清楚。4.1 名次到得分的映射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; // 无效名次统一按 0 分处理 }这段用六个独立 if而不是 if-else。因为成绩按名次互斥每条记录只会命中一个分支结果等价。但独立 if 有个隐患如果以后规则变成第 1 名 7 分第 2~3 名 5 分独立 if 就会重复赋值改成 if-else 能减少这类低级错误。更省事的写法是查表static const int score[6] {0, 7, 5, 3, 2, 1};然后point (num 1 num 5) ? score[num] : 0;。规则变化时只改表不动循环体。第 5 章会给出完整替换代码。4.2 学院分数的双重循环累加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]; // 团体总分 男 女 }外层循环遍历学院内层循环遍历所有运动员记录用学院编号匹配。复杂度是 O(n × 记录数)n≤20、记录数≤250最多 5000 次比较在当前规模下没有性能压力。这个结构的好处是对记录顺序不敏感只要学院编号对得上就能累加即使输入时学院交错出现结果也一样。因为 InitSCH 里学院编号从 1 开始连续分配college 表的下标 i 和学院号 collegenum[i] 有确定对应关系理论上可以直接用学院号定位学院行把内层循环省掉。但双重循环的可读性更好也不会因为学院编号跳号而出错作为课程设计是合理的选择。这里有个隐含约定名次超过 5 的记录 point 为 0累加时不会污染总分。所以在 mark 阶段把无效名次清零比在累加时判断只有 1~5 名才加更省事。4.3 output 的成绩单组织方式void output(student L1, college L2) { // 注意这里按值传参整表拷贝 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; } }output 的外层循环和 mark 的累加循环结构几乎一样都是按学院过滤运动员记录。差异在细节mark 用指针传参output 用按值传参。student 表包含 100 组字段按值传递会把整张表拷贝一份虽然能跑但没必要。改成const student或const college可以省掉这次拷贝也避免误修改原表。还有一处小问题表头里学院列打的是运动员自己的 collegenum[j]而不是外层循环的 i1。正常情况下两者相等但如果数据里混入脏记录成绩单会把脏记录的学院号也打出来在报表层面能直接发现前面校验漏洞留下的数据。4.4 用 n4、m3、w2 跑一遍验证按题目给的测试参数手工构造一组覆盖男女项目、不同学院、不同名次的输入4 3 2 1 zhangsan b 1 1 2 lisi b 1 2 4 wangfang g 1 1 1 sunqi b 2 3 4 zhaomin g 2 2 5 zhouba g 3 1 #前两行是 n 和 m第三行是 w。六条记录的含义项目 1男子学院 1 第 1 名得 7 分项目 2男子学院 1 第 2 名得 5 分项目 4女子学院 1 第 1 名得 7 分项目 1男子学院 2 第 3 名得 3 分项目 4女子学院 2 第 2 名得 5 分项目 5女子学院 3 第 1 名得 7 分。项目 3 和学院 4 没有记录正好验证空表输出。预期的团体总分报表学院男子总分女子总分团体总分112719235830774000跑程序时重点看三处学院 1 的成绩单应有 3 行得分分别是 7、5、7学院 3 只有女子项目记录男子总分为 0学院 4 整表为空但总分三列仍然是 0 而不是随机值。这三处通过说明 InitSCH 清零、mark 按性别累加、output 按学院过滤三条主链路都是对的。5. 收尾改进容量边界、名次上限与更干净的算分写法最后把代码里几个值得动手改的点列出来每一处都可以单独验证不影响原有输出。// 1. 容量MAXSIZE 100 装不下 (3020)*5250 条 #define MAXSIZE 300 // 或改用 vector彻底去掉 last 下标 // 2. 名次上限n 换成 5否则 n4 时第 5 名录不进 if (S1-num[i] 1 || S1-num[i] 5) { /* 重输 */ } // 3. 项目号上限补上 mw堵住项目号 99 这类脏数据 if (S1-item[i] 1 || S1-item[i] m w) { /* 重输 */ } // 4. 算分查表去掉六行 if static const int score[6] { 0, 7, 5, 3, 2, 1 }; for (int i 0; i L1-last; i) L1-point[i] (L1-num[i] 1 L1-num[i] 5) ? score[L1-num[i]] : 0;第一条可以直接把宏改成 300或者花半小时把 student 和 college 换成vector容器last 下标改成size()所有L1-last 1的地方同步改。第二条和第三条是修复校验逻辑改完用第 4 章的测试数据回归一遍输出应该和原来完全一致。第四条把 mark 函数从六行 if 压成三行规则变更时只需要改 score 数组。这类老代码在互联网上能找到大量同源版本互相之间经常抄来抄去边界条件和容量值各有各的错。拿到手先做两件事把校验规则逐条对照题目要求列成表再用最小测试集跑一遍回归。等 n4、m3、w2 的输出和你手工算的分一致了再去动容量和查表顺序千万别反过来。改完这些再把校验逻辑抽成独立的 checkRecord 函数input 就只剩下读取和调度两件事后面接文件导入或者图形界面都只动入口不碰统计逻辑。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →