简介:一份面向数据结构课程设计的完整《运动会分数统计》报告文档,适合计算机相关专业学生参考,尤其适合需要完成类似信息管理系统课程设计、掌握排序查找等知识点的读者。资源包内仅有1个doc文件,大小916KB,既包含课程设计报告正文,也附有源代码说明;目前已有三千三百八十七人学习下载。文档围绕运动会分数统计系统展开:先进行需求分析,再给出概要设计与详细设计,采用结构体数组保存项目及学校数据,利用文件读写函数实现持久化存储,使用冒泡排序实现按学校编号、总分、男女团体总分输出,并通过顺序表查询学校项目情况或按项目编号获取前三名或前五名。此外还配有输入输出、统计函数的设计思路以及合法、非法、局部非法数据的测试结果。全文从需求到实现再到调试总结,体系完整,可作为课程设计报告撰写和程序实现的直接参考。
1. 运动会分数统计是数据结构课设里的「中线题」:需求小但考察面全
运动会分数统计是数据结构课程设计里最经典的题型之一:录入各院系运动员在各个项目上的名次,按名次换算成分数,汇总出男团、女团和院系总分排行,再提供按项目和按院系的查询。题目不复杂,但正好把线性表、结构体、排序算法和文件持久化全部串起来,覆盖数据结构 c 语言版教材里最常考的几个章节,所以历届选题中它始终排在前几名。网上流传的版本不少是 .doc 格式的课程设计报告加源代码附件,报告里写需求分析、数据结构定义、流程图和测试用例,代码则是完整的 C 语言工程。我不打算在这里复述某一份现成文档,而是按自己做这套题目的思路,讲清楚数据怎么组织、排序怎么实现、文件怎么存、验收时哪些坑会被老师反复问。适合正在选课设题目,或者代码写了一半被指针和排序卡住的同学。
2. 数据怎么存:结构体数组比链表更合适,字段设计决定后面编码省不省事
运动会分数统计的数据量一般就几十个院系、十几个项目,属于典型的小规模数据。选型时不用刻意追求复杂,关键是访问方式。需求里有三类高频操作:按院系编号改成绩、按总成绩排序、按项目查名次。院系总数固定且编号唯一,增删频率极低,最常见的操作是随机访问和排序,所以顺序表(结构体数组)天然占优。链表写起来好看,但排序和随机访问都要遍历,代码量翻倍,课设验收时反而更容易暴露指针问题。这里的经验是:数据结构没有绝对好坏,贴合访问模式才是选型标准。
2.1 两张核心表:院系信息表和成绩表,先定字段再写代码
第一张表存院系基本信息,第二张表存每个项目的成绩记录。很多翻车现场都是字段没想清楚就动手写,写到统计总分时发现缺了性别字段,又回去改结构体,白白浪费时间。我用的是两个结构体:
#define MAX_DEPT 30 // 最多参赛院系数 #define MAX_EVENT 50 // 最多比赛项目数 typedef struct { int id; // 院系编号,从 1 开始 char name[24]; // 院系名称 int male_score; // 男团总分 int female_score; // 女团总分 int total_score; // 团体总分 } DeptInfo; typedef struct { int event_id; // 项目编号 char event_name[32];// 项目名称,比如 男子100米 int rank; // 该项目取得的团体名次 int dept_id; // 取得该名次的院系编号 int score; // 按名次换算后的分数 int is_male; // 1 表示男子项目,0 表示女子项目 } ScoreItem;DeptInfo 对应院系表,ScoreItem 对应成绩明细表。ScoreItem 里记 dept_id 而不是直接记院系名称,是为了避免名称冗余,统计总分时用 dept_id 去关联院系表。这是典型的关系型思维,放在 C 语言结构体里同样适用,后续无论做排序还是做查询,都能少写一半字符串比较代码。
两个宏 MAX_DEPT 和 MAX_EVENT 是后续所有数组下标的边界。改数据规模时只需要改这两行,不需要动任何循环逻辑,这是课设验收时可以主动讲的细节。另一个容易忽略的点是 ScoreItem 里把「名次」和「得分」分成两个字段,而不是只存得分。这样既能按名次输出名次表,也能在数据校验时反查分数算得对不对。如果把换算后的分数直接覆盖到 rank 上,后面想改计分规则就得重录数据,等于给自己断了后路。
2.2 为什么坚持用顺序表:访问模式决定选型,而不是「显得高级」
我见过不少参考源代码用链表实现院系信息管理,优点是看起来可以动态扩容。但对这份题目来说,院系数通常在程序运行前就能确定,由用户先输入参赛院系数 N。既然 N 已知,动态扩展就失去了意义,顺序表在内存连续性和访问效率上都更好。
排序环节直接决定最终排名输出。用结构体数组时,qsort 或手写排序都只需要交换结构体变量,一行赋值语句就能完成两个元素交换;换成单链表,交换节点还是交换数据域,都会引入额外的指针处理。老师验收时最常问的就是「为什么用顺序表不用链表」,能回答出「数据规模固定、随机访问频繁、排序成本低」这三点,基本就过关了。反过来,如果题目明确要求院系数量在运行时动态增减,我才会考虑链表方案,这是由题目的访问模式决定的,不是越复杂的结构越加分。
顺序表另一个好处是文件持久化简单。fwrite 可以直接按结构体大小写一整块内存,读回来也一样;链表则需要逐个节点序列化。第 4 章会专门讲文件部分,这里先记住结论:这份题目用结构体数组,代码量最少,逻辑最不容易出错。很多同学把课设做成「为了用链表而用链表」,结果排序环节成了全场 bug 最多的地方,完全没必要。
2.3 最小可运行的院系录入代码:先让系统跑起来,再往上加功能
拿到题目先别急着写完所有功能,先把「录入 N 个院系 → 打印列表」这条主链路跑通。这一步能验证结构体和输入输出逻辑没问题,后面所有功能都基于此扩展。下面这段代码可以直接复制到你的源代码文件里作为骨架。
#include <stdio.h> #include <string.h> #define MAX_DEPT 30 typedef struct { int id; char name[24]; int male_score; int female_score; int total_score; } DeptInfo; int main() { DeptInfo depts[MAX_DEPT]; int n, i; printf("请输入参赛院系数: "); while (scanf("%d", &n) != 1 || n <= 0 || n > MAX_DEPT) { printf("院系数需在 1 到 %d 之间,请重新输入: ", MAX_DEPT); while (getchar() != '\n'); // 清空输入缓冲,防止残留字符影响下次读取 } for (i = 0; i < n; i++) { depts[i].id = i + 1; printf("请输入第 %d 个院系名称: ", i + 1); scanf("%23s", depts[i].name); // %23s 防止超长输入覆盖结构体其他字段 depts[i].male_score = 0; depts[i].female_score = 0; depts[i].total_score = 0; } printf("录入完成,当前院系列表:\n"); for (i = 0; i < n; i++) { printf("%d %s\n", depts[i].id, depts[i].name); } return 0; }这段代码里有两个参数值得说明。scanf("%d", &n) 的返回值判断,让非数字输入不会被静默接受,配合内层 while (getchar() != '\n') 清空缓冲,避免下一次 scanf 读到上次输入的残留字符。scanf("%23s", name) 中的 23 是字符数组长度 24 减 1,防止用户超长输入时把名称尾部覆盖到结构体后面的内存,这是 C 语言课堂不常讲但实际开发必须处理的边界。
主循环里先把三个分数字段全部初始化为 0,也是个容易忽略的步骤。结构体数组声明后是栈上的随机值,不初始化的话,后面统计时会把垃圾值累加进去,表现成「总分莫名其妙特别大」。初始化放在录入院系时一次做完,比统计函数里再补判空省事得多。这段骨架跑通后,再把成绩录入、统计、查询、文件保存几个模块按菜单填进去,整个工程的结构就很清晰了。
3. 名次转分数与总分排序:把计分规则做成参数,排序算法两种写法都给
计分规则是这道题的核心业务逻辑,也是最容易被需求描述坑到的地方。常见的计分表有两种:一种取前五名,按 7、5、3、2、1 计分;另一种取前八名,按 9、7、6、5、4、3、2、1 计分。题目可能还会加限制条件,比如同一院系在一个项目里最多取两人成绩,或者按报名表预先确定参赛人数。把这些规则做成可配置的全局数组和参数,而不是硬编码在业务逻辑里,是让代码具备可维护性的关键。这样无论规则怎么变,改的只是数据,不是流程。
3.1 计分表设计:前几名、各名次分值、性别分组,三个参数分开配
我习惯把计分表定义成全局常量数组,并在文件头部注释说明规则来源。这样答辩时评委问「如果规则改成取前六名怎么改」,你只需要改一处数组长度和内容,运行时不用重新编译逻辑代码。
// 计分规则:取前 5 名,分值依次为 7, 5, 3, 2, 1 #define RANK_LIMIT 5 const int score_table[RANK_LIMIT] = {7, 5, 3, 2, 1}; // 根据名次得到分数,第 6 名及以后不得分 int score_for_rank(int rank) { if (rank >= 1 && rank <= RANK_LIMIT) { return score_table[rank - 1]; } return 0; }score_table[rank - 1] 用名次做下标,是因为名次从 1 开始而数组下标从 0 开始。这个「名次减一」的映射几乎出现在每一份参考源代码里,同时也是新手高频错误:写成 score_table[rank] 就会数组越界,取到相邻内存的垃圾值。score_for_rank 开头的越界判断必须保留,因为录入环节可能产生非法名次,查询和统计环节也可能会拿脏数据来算分。
这里值得多提一句:计分规则要改成参数而不是深埋在统计函数里。有的同学在算分时直接写 if (rank == 1) score = 7; else if (rank == 2) score = 5; 功能上没错,但换个计分表就要改一长串分支,而且每个分支都可能漏改。用数组做映射,规则变化只动 score_table。这种「数据驱动」的小习惯,在后面对 ScoreItem 做批量换算时会非常省心,也更接近真实项目里配置项分离的做法。
3.2 总分聚合与排序:手写冒泡是基本功,qsort 是工程写法,两个都得会
聚合阶段的逻辑是:遍历 ScoreItem 数组,根据 is_male 字段把 score 累加到对应院系的 male_score 或 female_score,最后把两者相加得到 total_score。代码本身不复杂,但要注意按 dept_id 定位院系时,最好用下标直接索引,前提是院系编号与数组下标严格对应,即 1 号院系存在 depts[0] 这个约定。
void aggregate(DeptInfo *depts, int dept_n, ScoreItem *items, int item_n) { int i, d; // 先清零,避免重复调用时分数叠加 for (i = 0; i < dept_n; i++) { depts[i].male_score = 0; depts[i].female_score = 0; depts[i].total_score = 0; } for (i = 0; i < item_n; i++) { d = items[i].dept_id - 1; // 院系编号转数组下标 if (d < 0 || d >= dept_n) { printf("成绩记录 %d 的院系编号 %d 非法,已跳过\n", i, items[i].dept_id); continue; } if (items[i].is_male) { depts[d].male_score += items[i].score; } else { depts[d].female_score += items[i].score; } depts[d].total_score += items[i].score; } }注意 aggregate 函数开头先统一清零。如果没有这一步,用户在菜单里连续触发两次统计,分数就会翻倍。这也是数据结构和算法题目里「可重入」思想的体现:同一个函数被调用多次,结果应该一致,不能依赖数组初始状态。还有一个细节是 items[i].dept_id - 1 之后立刻做边界检查,一旦成绩文件里混入不存在的院系编号,程序会打印提示并跳过,而不是直接越界崩溃。
排序这里我写两种,答辩时老师大概率会问「你会几种排序」。手写冒泡排序考察基本功,qsort 体现工程能力:
#include <stdlib.h> // 手写冒泡:按 total_score 降序排列 void bubble_sort_dept(DeptInfo *depts, int n) { int i, j; DeptInfo tmp; for (i = 0; i < n - 1; i++) { for (j = 0; j < n - 1 - i; j++) { if (depts[j].total_score < depts[j + 1].total_score) { tmp = depts[j]; depts[j] = depts[j + 1]; depts[j + 1] = tmp; } } } } // qsort 比较函数:总分降序,总分相同时院系编号升序 int cmp_dept(const void *a, const void *b) { const DeptInfo *pa = (const DeptInfo *)a; const DeptInfo *pb = (const DeptInfo *)b; if (pa->total_score != pb->total_score) { return pb->total_score - pa->total_score; } return pa->id - pb->id; }冒泡排序的时间复杂度是 O(n^2),对几十个院系完全够用。内层循环的 j < n - 1 - i 是标准写法,每一轮把当前未排序列的最小值沉到末尾,整体呈降序输出。qsort 版的比较函数返回值语义是:返回值小于 0 时,a 排在 b 前面。这里刻意处理了总分相同的情况,返回 pa->id - pb->id,让编号小的排前面。这能避免一个常见问题:总分相同时两次运行排名可能会不同,看起来像随机数玄学。
3.3 查询功能:按项目查名次、按院系查明细,遍历数组就是全部逻辑
查询功能通常有两个入口:输入项目编号,输出该项目全部名次及对应院系;输入院系编号,输出该院系所有获奖项目和得分。两个功能本质都是对 ScoreItem 数组做一次遍历,找到匹配记录逐条打印。
void query_by_event(int event_id, ScoreItem *items, int item_n) { int i, found = 0; printf("项目 %d 名次列表:\n", event_id); for (i = 0; i < item_n; i++) { if (items[i].event_id == event_id) { printf(" 第 %d 名: 院系 %d, 得分 %d\n", items[i].rank, items[i].dept_id, items[i].score); found = 1; } } if (!found) { printf(" 未找到该项目成绩\n"); } }这个函数逻辑很直白,两点值得说。第一,遍历找到所有匹配记录而不是找到一条就返回,因为一个项目下有多条名次记录。第二,found 标志位用来区分「查无结果」和「查询成功」,而不是靠 printf 有没有输出判断。很多同学的代码在查询无结果时不提示任何信息,演示时输入一个不存在的项目号,界面毫无反应,评委只会觉得程序坏了。按院系查询的写法完全对称,只是把判断条件改成 dept_id 相等。两个查询函数加起来不到四十行,却是菜单里被点得最频繁的功能,值得把它们单独拆成函数而不是揉在主函数里。
4. 文件存取与数据校验:二进制还是文本,先想清楚谁来读这个文件
课设要求里通常有一条「将数据保存在文件中」,这一条看似简单,实际上决定了程序演示时的体验。如果每次运行都要重新录入几十条成绩,演示现场会非常尴尬。文件模块要解决的核心问题有两个:用什么格式存储,以及读取时如何校验数据完整性。下面的方案都基于 C 标准库,不需要引入任何第三方依赖,在任何支持 C89 的编译器上都能直接编译运行。
4.1 格式选型:文本可读性好、便于改错;二进制省心、但不适合跨机器交换
常见做法是存文本文件,因为老师可能会打开数据文件检查内容。用 fprintf 按行写,每个字段用空格分隔,读回时用 fscanf 按同样顺序解析。好处是人眼能读懂,如果数据文件被手工改错,也能对照着修正;坏处是解析时对空格和换行敏感,容易出现「多了一个空格就读错数据」的情况。
另一种是二进制文件,fwrite 和 fread 直接读写结构体内存块。好处是读写代码极短,性能也高;坏处是文件不可读,更不能保证换一个编译器或机器后还能正确打开,因为不同平台对结构体字节对齐的处理不同,sizeof(DeptInfo) 可能不一样。我自己在这份课设里优先用文本格式,理由只有一个:课设需要展示,数据文件可读能让老师一眼看懂程序到底存了什么。真到了几万条成绩的性能场景,才值得用二进制。
void save_to_file(DeptInfo *depts, int dept_n, ScoreItem *items, int item_n, const char *path) { FILE *fp = fopen(path, "w"); if (fp == NULL) { perror("打开文件失败"); return; } fprintf(fp, "DEPT %d\n", dept_n); for (int i = 0; i < dept_n; i++) { fprintf(fp, "%d %s %d %d %d\n", depts[i].id, depts[i].name, depts[i].male_score, depts[i].female_score, depts[i].total_score); } fprintf(fp, "ITEM %d\n", item_n); for (int i = 0; i < item_n; i++) { fprintf(fp, "%d %s %d %d %d %d\n", items[i].event_id, items[i].event_name, items[i].rank, items[i].dept_id, items[i].score, items[i].is_male); } fclose(fp); }这一段有两个习惯值得学。第一,用 DEPT 和 ITEM 前缀充当段标记,读取时先读段标记再决定后面解析多少行,这让文件格式有自描述能力,以后加新字段不用破坏旧文件的读取逻辑。第二,每个 fprintf 都写完整一整行,字符串字段放在中间位置。注意院系名和项目名里不能有空格,否则按空格分隔的解析会错位,所以录入时我限制了名称只能是连续字符,或者用下划线连接。
读取代码比写入更考验耐心,因为文件可能被手工编辑过,也可能不完整。load 函数必须对每一段做标记校验和数量校验:
void load_from_file(DeptInfo *depts, int *dept_n, ScoreItem *items, int *item_n, const char *path) { FILE *fp = fopen(path, "r"); if (fp == NULL) { perror("打开文件失败"); return; } char tag[16]; int n = 0; fscanf(fp, "%15s %d", tag, &n); if (strcmp(tag, "DEPT") != 0 || n <= 0 || n > MAX_DEPT) { printf("文件格式错误: 院系段标记不对或数量越界\n"); fclose(fp); return; } *dept_n = n; for (int i = 0; i < n; i++) { fscanf(fp, "%d %23s %d %d %d", &depts[i].id, depts[i].name, &depts[i].male_score, &depts[i].female_score, &depts[i].total_score); } fscanf(fp, "%15s %d", tag, &n); if (strcmp(tag, "ITEM") != 0 || n < 0 || n > MAX_EVENT) { printf("文件格式错误: 成绩段标记不对或数量越界\n"); fclose(fp); return; } *item_n = n; for (int i = 0; i < n; i++) { fscanf(fp, "%d %31s %d %d %d %d", &items[i].event_id, items[i].event_name, &items[i].rank, &items[i].dept_id, &items[i].score, &items[i].is_male); } fclose(fp); }这段代码里的 %15s、%23s、%31s 限长格式符,作用与 scanf 场景相同:防止从文件读入超长字符串时溢出缓冲区。每次 fscanf 后应该检查返回值是否等于期望的字段数,如果文件被截断或混入不可见字符,fscanf 会返回 EOF 或更少的匹配数,程序应当立刻报错,而不是继续用垃圾数据往下算。这是文件模块里最重要的防御式编程习惯,能挡掉一半以上的演示事故。
4.2 数据校验:数量越界、编号不存在、计分超过上限,读入时一次查完
文件读入后不要直接开始排序,先跑一遍完整校验。我把校验逻辑放在 load 之后、主菜单之前,所有非法数据打印到屏幕,提示用户手工修正文件或重新录入。三个必查项是:院系编号是否落在合法范围、成绩记录里的 dept_id 是否有对应院系、score 是否大于计分表里的最高分。下面这段校验函数是关键片段。
int validate_data(DeptInfo *depts, int dept_n, ScoreItem *items, int item_n) { int i, errors = 0; for (i = 0; i < item_n; i++) { if (items[i].dept_id < 1 || items[i].dept_id > dept_n) { printf("第 %d 条成绩: 院系编号 %d 不存在\n", i, items[i].dept_id); errors++; } if (items[i].score < 0 || items[i].score > score_table[0]) { printf("第 %d 条成绩: 得分 %d 不在合法范围(0~%d)\n", i, items[i].score, score_table[0]); errors++; } if (items[i].rank <= 0) { printf("第 %d 条成绩: 名次 %d 非法\n", i, items[i].rank); errors++; } } return errors; }注意这里 score > score_table[0] 的检查用 score_table[0] 而不是写死 7,因为计分表的第一项永远是最高分,规则改动时校验代码不用跟着改。这是把规则参数化的另一个好处:规则、统计、校验都复用了同一份数据源,不会出现一边改了一边没改的情况。
文件模块最容易被忽略的是「读入后内存里的数据是否可信」这个问题。不校验直接进入查询和统计,遇到脏数据时程序可能输出完全不合理的排名,评委的第一反应是「代码错了」而不是「数据错了」。提前写一个 validate_data,在程序入口调用并检查返回值,能避免演示时当场翻车。
5. 避坑清单:课设演示最容易翻车的五个问题,从现象查到根因
这一章的五条坑,都是我实际改代码时反复遇到过的,每一条都按「现象 → 原因 → 解决」的思路写。第 5 章就当质检清单用,每改完一段代码,对照着查一遍。
5.1 项目录完一排序,院系编号和总分全对不上
最常见的是录入顺序是 1 号、2 号、3 号院系,排序输出后编号顺序变成 3、1、2,而且每个编号后面的总分跟排序前完全对不上。原因出在排序时交换了整个结构体数组,排序后 depts[i] 里的 id 不再等于 i + 1,后续统计功能如果直接用下标 depts[i].id 去访问,自然就找错对象。
解决办法是让所有访问都走 id 字段而不是依赖下标。统计总分时通过 depts 数组查找对应 id 的院系,排序时也只比较 total_score,交换的是整个结构体不改变 id 属性。如果代码里还有大量 depts[i].id == i + 1 的假设,要么在录入阶段保证 id 与下标一致,要么把所有假设改成显式比较,否则排序功能一上线就会爆发连锁错误。
5.2 总分相同,两次排序结果不一样
现象是 A 院系和 B 院系总分都是 15 分,第一次运行 A 排在前面,第二次运行 B 排在前面。原因很简单:冒泡排序在总分相等时不做任何处理,交换与否取决于数组原始排列,这不是排序算法本身的缺陷,而是缺少明确的次级排序键。
解决方式是在比较逻辑里加入编号作为第二排序条件,总分相同时编号小者在前。qsort 版里就是 cmp_dept 最后的 return pa->id - pb->id。这看起来是小细节,但评委如果连续按两次排名菜单发现排名变了,会直接质疑整个排序算法的正确性,所以省掉这行代码非常不值得。
5.3 fscanf 读文件时字符串和数字错位
现象是成绩文件加载后,院系名变成了一串数字,或者某个项目的名称变成了上一个项目的名称。原因几乎都是保存和读取的格式串不一致:fprintf 写出去的字段顺序是「编号 名称 分数」,读取时如果漏写了 %s 或者把顺序写反,字符串就会被当作数字解析,后面的字段全部错位。
解决方式是让 save 和 load 共用同一份格式注释,一个字段一个字段对齐检查。我的土办法是在两个函数上方各写一行注释注明「写入顺序:id name male female total」,然后逐字段对照。更省心的是每行只存一条完整记录,字符串一律用 %s 或限长 %31s 读取,不让字符串包含空格,从源头消除分隔歧义。
5.4 输入名次 9,算出个离谱分数
现象是老师随手输入一个名次 9,程序不报错,最后该院系的成绩单上出现一个明显不对的分数。原因在于 score_for_rank 函数只校验了 rank 是否在 1 到 RANK_LIMIT 之间,如果调用方传入了负数或超大值,score_table[rank - 1] 就会数组越界,读到的可能是相邻内存里的随机值,表现为正分、负分或者很大的数。
解决方法是把越界检查写三层:录入成绩时用 while 循环限制名次范围;score_for_rank 入口处对 rank <= 0 或 rank > RANK_LIMIT 直接返回 0;文件校验时再对 rank 字段做一次合法性检查。越界检查写三遍都不嫌多,因为每一层都可能放过不同的非法输入来源。
5.5 数组越界但不崩,程序跑完结果全错
现象是程序运行不报错,但输出里出现一个「幽灵院系」,分数巨大,或者项目列表里多出一条不存在记录。原因是 C 语言数组越界是未定义行为,不一定会触发段错误,更常见的是把相邻内存的数据读出来。MAX_DEPT 定义为 30,录入 31 个院系时,第 31 个结构体就写到了相邻内存上,后续所有读取都可能在错误数据上运算。
解决方式是在所有循环里坚持用 i < n 并且先校验 n 的范围;录入院系数时用 while 循环强制 n 不超过 MAX_DEPT。还有一个血泪经验:可以在 depts 数组声明为 depts[MAX_DEPT + 1],并手动把下标 30 的位置填成 id = 0 的哨兵,一旦有越界写入,检查哨兵被改动就能立刻定位问题。越界不报错的 bug,比直接崩溃难查十倍,宁可多写几行防御代码。
6. 答辩前自检:用三个边界场景验证你的源代码,顺便准备两个加分回答
课设评分不只看功能是否完整,还看你对代码的熟悉程度和边界处理习惯。我自己答辩前会准备一个临时测试文件,把三个边界场景各跑一遍。第一,录入 1 个院系和正好 30 个院系,确认极端数量下不会越界;第二,录入一个项目 30 人参赛并只取前 5 名计分,确认 score_for_rank 对第 6 名返回 0,后面名次不会影响总分;第三,手工编辑数据文件,把某个成绩记录的院系编号改成 99,确认 validate_data 能报错提示而不是让程序崩溃。这三个场景的输出结果,也正好可以作为数据结构实验报告里的测试用例部分,比空写「系统测试通过」有说服力得多。
有两个加分回答值得提前准备。第一个是「为什么用结构体数组而不用链表」,围绕数据规模固定、随机访问多、排序成本低这三点展开;第二个是「如果参赛院系数量不固定,你的系统怎么扩展」,可以答改成动态数组或链表,并指出需要同步修改统计、排序、文件读写三个模块。这两个问题答得顺,比现场多写一百行代码更能得分。我在自己做的版本里还给排名功能加了一个小特性:总分相同的院系自动显示「并列第 X 名」,实现只要在排序后比较相邻两个院系的总分,代码不到 20 行,但演示效果很有记忆点。
最后分享一条我做课设攒下的教训:别把源代码和实验报告分开写,先让报告里的数据结构定义和代码里的结构体保持一致,再动手写功能。因为报告往往是代码的老祖宗,代码改完报告没改,答辩时老师看着两张不一致的表提问,场面会很尴尬。把这一条习惯保持到后续的课程设计和毕业设计里,能省下大量返工时间。希望帮到你。
本文还有配套的精品资源,点击获取