资讯详情

C语言结构体数组经典习题:学生成绩录入、平均分计算与最高分查找

📅 2026/10/13 22:39:05 | 华诺云谱 👁 阅读
C语言结构体数组经典习题:学生成绩录入、平均分计算与最高分查找
这道题是C语言学到结构体和数组时几乎人人都要写一遍的经典习题。从键盘录入3个学生的学号、姓名和两门课成绩算平均分再找出最高分学生并输出完整数据——听起来挺简单但我见过不少同学在这道题上交出能运行、结果却不对的代码也见过有人为了不用结构体硬是写了十几个变量把代码撑到一百多行。这篇文章就把这道题的完整写法拆开讲清楚从数据结构设计、输入输出细节到最高分判定一步一步说透适合刚学完结构体、想找一个完整练习的初学者也适合在考试前想快速复习这道题的同学。1. 这道题到底在考什么从输入到输出的三个关卡1.1 三件事拆开看录入、计算、输出先别急着写代码把题目拆成三个动作第一录入3个学生的数据。每个学生的数据包含学号、姓名、2门课程成绩一共四个信息录三次就是十二次输入。第二计算每个学生2门课的平均成绩。这里“平均成绩”是每门课成绩的平均不是全班平均看清楚描述别算错对象。第三找出最高分学生并输出他的学号、姓名、2门课成绩、平均分数。“最高分”三个字容易让人以为在比较某一门课但结合题目最后要求“输出平均分数”来看比较的是总成绩或者平均分。因为每个人都是2门课总成绩排序和平均分排序完全等价。这三个动作看起来各自独立实际是串成一条线的录入的数据要存起来存起来才能算平均分算完平均分才能比较大小比较完大小才能输出。所以核心不是“会不会写scanf和printf”而是“数据怎么存、怎么才能在不同步骤之间来回访问”。1.2 数据结构先行为什么不能靠变量名硬扛很多初学者拿到这道题的第一反应是定义变量int stu1_id, stu2_id, stu3_id; char stu1_name[20], stu2_name[20], stu3_name[20]; double stu1_score1, stu1_score2; double stu2_score1, stu2_score2; double stu3_score1, stu3_score2;这样写3个学生还能忍但代码已经非常难看。算平均分的时候三个学生要写三遍几乎一样的公式找最高分的时候要拿六个成绩互相比较if嵌套到头晕。万一题目改成30个学生这种写法直接崩盘。正确的做法是先用结构体把“一个学生的学号、姓名、两门课成绩”打包成一个整体。这就像你出门不会把身份证、手机、钥匙分别放在三个口袋里然后满世界找而是放进一个包里一提包所有东西都跟着走。结构体就是那个包它把逻辑上属于同一个对象的数据牢牢绑在一起之后不管传递到哪里都是一整块不会丢三落四。所以这道题表面在考printf、scanf、for循环、if比较其实更重要的考点是你有没有数据建模的意识能不能把一个真实世界中的“学生”抽象成程序里的一个类型。这才是结构体这道题想让你练的东西。2. 结构体设计把学号、姓名、成绩打包成一整块2.1 字段类型怎么选学号是int还是字符串成绩用float还是double定义结构体的时候每个字段选什么类型值得停下来想清楚。学号教材习题通常给的是1001、1002这种编号用int完全没问题。但如果你做过一点点真实项目就会知道实际系统的学号往往是“202400101”这样的字符串可能带前导零也可能含字母。用int存前导零会直接丢掉所以工业代码里学号一般用字符数组。这道题按习题标准用int最简单但代码里我会给字段命名时留好注释让你知道真实场景该怎么做。姓名用char数组长度给你习惯的20或30。有一个细节C语言里一个中文字符在GBK编码下占2个字节在UTF-8编码下占3个字节char数组的长度要留够。练习题里学生名字通常比较短char name[20]足够如果你要处理特别长的名字再加大即可。两门课成绩这是很多人栽跟头的地方。如果成绩用int85.5这种带小数的成绩直接丢了小数部分算平均分根本不准确。虽然考试输入往往是“89 92”这种整数但让我建议直接定义成double。理由很简单成绩字段用double录入时用%lf读输出用%.2f没有任何额外成本却能把小数成绩、平均分的小数都照顾到为什么不用平均分字段结构体里我建议额外加一个avg字段。因为找最高分学生时需要反复比较平均分如果不在结构体里存就得在比较时临时算一遍三个学生还好三百个学生就浪费大量重复计算。录入结束后立刻把平均分算出来存进去后续所有步骤直接访问avg就够了。struct Student { int id; // 学号 char name[20]; // 姓名 double score1; // 课程1成绩 double score2; // 课程2成绩 double avg; // 两门课平均成绩 };2.2 结构体数组的内存布局与初始化一个学生用结构体三个学生就要用结构体数组。定义方式非常简单struct Student students[3];这一行代码在内存里做了什么简单说它连续分配了三块结构体大小的空间students[0]、students[1]、students[2]在内存中紧挨着排布。你可以把这三块空间想象成柜子里的三个文件档案盒每个档案盒里分了好几个格子学号一格、姓名一格、成绩两格、平均分一格。访问某个学生的某个字段用点号运算符students[0].id 1001; strcpy(students[0].name, 张三); students[0].score1 85.5;注意name是字符数组不能直接写students[0].name 张三而要使用strcpy函数这一点初学者特别容易错。如果你只是在scanf里通过%s读入姓名那直接传给students[i].name就行因为数组名本身就可以作为字符串写入的目标位置。结构体数组的初始化也有两种方式。一种是在定义时用花括号给初值适合数据已知的情况struct Student students[3] { {1001, 张三, 89.0, 92.0, 90.5}, {1002, 李四, 78.0, 85.0, 81.5}, {1003, 王五, 95.0, 88.0, 91.5} };另一种是本题的实际场景从键盘录入数据所以先定义空数组再用循环逐个赋值。两种方式对应不同场景都是基本功。3. 输入环节的坑scanf的换行符、缓冲区与中文姓名3.1 一条scanf读完一行数据连带跳空白录入代码看起来很简单就是在一个for循环里反复scanffor (i 0; i 3; i) { printf(请输入第%d个学生的学号、姓名、两门课程成绩\n, i 1); scanf(%d %s %lf %lf, students[i].id, students[i].name, students[i].score1, students[i].score2); }这里有个新手最容易疑惑的点为什么格式串里要写空格因为scanf的%d、%s、%lf在读取之前都会自动跳过输入流里的空白字符包括空格、制表符和换行。所以用户在键盘上输入“1001 张三 89 92”再回车或者分成四行输入效果都一样scanf都能正确把四个值分别读入四个变量。如果格式串里只写%d%lf这种不加空格的比如scanf(%d%lf, a, b)那么用户输入“123 45.5”时%d读走123%lf遇到空格会跳过也能读走45.5。所以现代C标准里%d这类格式符本身就会跳过空白格式串里的空格主要是为了可读性。真正麻烦的是%c它不会跳过空白。如果你用scanf(%c, ch)而前面刚读过一个数字缓冲区里残留的换行符会被%c直接读走造成“输入被跳过”的错觉。这道题不用%c读字符所以这个坑我们先记着后面调试时你会看到它的影子。3.2 中文姓名、带空格姓名和fgets的选择用%s读姓名有一个限制遇到空格就会停止读取。如果学生姓名是英文的“Alice”没问题如果是中文“张三”键盘输入时中间没有空格也没问题。但如果哪天遇到“Jean Luc”这种中间带空格的西方姓名%s只能读到“Jean”后面的“Luc”会被当成下一个成绩去解析程序当场乱掉。处理带空格的字符串C语言标准库里有两个选择。一个是gets但gets在C11标准里已经被正式废弃因为它无法限制读取长度输入太长会造成缓冲区溢出属于严重安全漏洞。另一个是fgets它可以指定读取的最大字符数安全得多fgets(students[i].name, sizeof(students[i].name), stdin);注意fgets会把换行符也读进数组所以用完之后通常要手动去掉末尾的换行符students[i].name[strcspn(students[i].name, \n)] \0;这道题是教材习题我默认姓名不含空格直接用%s是最简洁的做法重点放在题目本身的数据结构上。但我强烈建议你记住fgets这个工具以后任何一次真实项目里需要读一行可能包含空格的用户输入时你都会感谢今天记住了它。输入部分还有一个容易被忽略的体验问题一定要加提示语。黑乎乎的终端窗口里如果没有printf提示用户根本不知道程序在等他输入什么。好的输入提示是程序的第一步人性化设计。你写printf(请输入第%d个学生的学号、姓名、两门课程成绩\n, i 1);哪怕循环走三次用户也能清楚知道自己正录到第几个学生。4. 平均分与最高分判定两个容易想当然的地方4.1 平均分计算整数除法的陷阱平均分公式是两门课成绩相加再除以2看起来是小学数学但在C语言里有个经典陷阱整数除法。如果你把成绩定义成int这样写int score1 89, score2 92; double avg (score1 score2) / 2;结果是多少89加92等于181181除以2等于90.5但C语言里两个整数相除结果还是整数小数部分直接截断得到90。再把90赋给double类型的avg得到的是90.0不是90.5。解决办法有两个。第一把成绩定义成double这我在设计结构体时已经做了double类型的score1加score2除以2.0浮点数参与运算结果天然带小数students[i].avg (students[i].score1 students[i].score2) / 2.0;第二如果成绩必须是int类型那除法时让其中一个操作数变成浮点数比如除以2.0或者先把结果乘以1.0再除。总之记住一句话只要你想得到小数结果除法里至少要有一个浮点数参与。我见过同学为这个问题调试很久最后输出的平均分全是整数根本原因就是没有意识到int和int相除会截断小数。这类错误非常隐蔽因为程序不报错逻辑也看不出问题只有对着一组带小数点的成绩手工验证才能发现。4.2 最高分到底是哪门课最高题目意图与比较策略题目写“最高分的学生”这个“最高分”有两种理解第一种是某门课成绩最高第二种是总成绩或平均分最高。结合题干最后那句“输出最高分学生的数据包括平均分数”标准答案显然按第二种理解处理。因为如果只看某一门课那平均分信息在这个判断里就派不上用场了题目为什么要让你输出平均分所以比较对象确定为学生的平均分avg。确定比较对象之后写代码找最高分学生的算法就非常朴素了假设第一个学生是最高分用一个变量记录他的下标然后从第二个学生开始遍历一旦发现有人的平均分比当前最高分还高就更新这个下标。三行核心逻辑int maxIndex 0; for (i 1; i 3; i) { if (students[i].avg students[maxIndex].avg) { maxIndex i; } }这里有另一个细节比较时用还是效果有区别。用意味着只有严格大于当前最高分时才更新下标两个学生并列最高时最终选中的是第一个。用遇到并列最高时会更新成后一个最终输出的是最后一个。题目没有规定并列时的输出策略为了逻辑稳定我推荐用让代码在并列时输出第一个满足条件的学生行为可预测也容易向别人解释。如果你希望把并列最高分的所有学生都输出那就得换成两轮遍历第一轮找出最高分是多少第二轮把等于这个分数的学生全部打印出来。这种“多峰”场景在很多真实系统里会用到但作为这道习题的参考答案用记录下标最简单直接先把主干逻辑跑通进阶需求放到后面再说。5. 完整参考代码一份能直接运行的C语言程序5.1 整段代码先放出来把前面所有设计串起来完整代码如下这份代码可以直接复制到你的编译器里运行#include stdio.h struct Student { int id; // 学号 char name[20]; // 姓名 double score1; // 课程1成绩 double score2; // 课程2成绩 double avg; // 两门课平均成绩 }; int main() { struct Student students[3]; int i; // 录入3个学生的数据 for (i 0; i 3; i) { printf(请输入第%d个学生的学号、姓名、两门课程成绩\n, i 1); scanf(%d %s %lf %lf, students[i].id, students[i].name, students[i].score1, students[i].score2); // 录入后立即计算平均分存在结构体字段里 students[i].avg (students[i].score1 students[i].score2) / 2.0; } // 输出每个学生的平均成绩 printf(\n每个学生的平均成绩\n); for (i 0; i 3; i) { printf(学号%d姓名%s平均分%.2f\n, students[i].id, students[i].name, students[i].avg); } // 找出平均分最高的学生下标 int maxIndex 0; for (i 1; i 3; i) { if (students[i].avg students[maxIndex].avg) { maxIndex i; } } // 输出最高分学生的完整数据 printf(\n最高分学生的数据\n); printf(学号%d姓名%s课程1%.2f课程2%.2f平均分%.2f\n, students[maxIndex].id, students[maxIndex].name, students[maxIndex].score1, students[maxIndex].score2, students[maxIndex].avg); return 0; }5.2 逐段走读每行代码为什么这么写结构体定义放在main函数外面这是必须的。结构体是一个类型声明如果定义在main函数内部它的作用域就只在main函数里虽然这道题只有一个函数也能跑但养成类型定义放外层的好习惯以后写多文件项目时类型能在整个翻译单元里复用。main函数里第一件事是定义结构体数组students[3]。这里没有初始化因为数据要靠键盘输入。紧接着进入录入循环循环体里先printf提示再scanf读取最后立刻计算avg存回结构体。把avg的计算放在录入循环内可以让每条学生数据在录入完成的那一刻就是完整的后续不需要再回头补算。第二条循环负责输出每个学生的平均成绩。这里用%.2f控制精度输出两位小数是成绩展示的常规做法。不要用%f直接输出%f默认打印6位小数91.500000这种结果在成绩场景里很难看。第三条循环找最高分这里再强调一下下标初始化。maxIndex 0表示先默认第0个学生最高然后循环从i 1开始逐个和当前最高比。注意循环边界是i 3也就是只比较下标1和2不会越界也不重复比较下标0。如果把循环写成i 3就会访问students[3]那是数组外的内存属于未定义行为严重时程序会崩溃。6. 测试用例怎么设计用四组数据把边界情况试一遍6.1 一组好数据胜过十次“看起来能跑”很多同学写完程序随手输入三组数据看到输出正常就说“程序对了”。但真正可靠的程序要拿四组不同性质的数据去试。第一组是常规数据验证基本功能。三个学生成绩各不相同最高分一目了然学号姓名课程1课程21001张三89921002李四78851003王五9588预期输出张三平均90.50李四平均81.50王五平均91.50最高分学生是王五。第二组数据要含小数成绩专门验证浮点数计算有没有出问题学号姓名课程1课程22001赵六85.5902002钱七8888第三组数据是边界情况最多名次并列最高验证并列时程序不会报错学号姓名课程1课程23001孙八80903002周九8585两个学生平均分都是85.0用比较时程序应该稳定输出第一个学生孙八。第四组是刁钻数据输入成绩为0或者满分学号姓名课程1课程24001吴十004002郑一100100这两个极端值能暴露出除法和比较逻辑是否健壮。平均分为0.00的学生不会被误判为最高分平均分100.00的学生能正常被选中。6.2 实测中常见的三种异常表现我让几位初学阶段的朋友实际跑过这份代码遇到最多的问题集中在三种情况。第一种输入过程中少打了一个数字程序后面的数据全部错位。比如第一个学生本来要输入“1001 张三 89 92”结果漏了92那第二个学生的学号就会被scanf当成第一个学生的课程2成绩读进去。这种错误几乎无解因为程序无法智能判断用户本意只能靠录入阶段的提示和复查来尽量避免。更稳妥的做法是每录完一个学生立刻把这一条数据显示出来让用户确认但这种交互逻辑会让题目代码变复杂作为习题可以暂时不做。第二种在Windows的命令行窗口里运行完窗口一闪而过什么都看不到。这是运行环境的问题不是代码逻辑的问题。在Dev-C里直接编译运行程序结束窗口会自动关闭。临时解决办法是在return 0之前加一句getchar()让程序等待按键但这个方案需要小心缓冲区里可能残留的换行符所以更推荐在IDE里设置“运行后暂停”或者直接在在线C语言编译环境里跑省去本地环境配置的麻烦。第三种中文姓名在控制台里显示成乱码。这也是Windows中文版控制台的经典问题代码里用的是UTF-8或者GBK控制台默认代码页可能不匹配。这个问题和程序逻辑无关但非常打击初学者的信心。如果你遇到了先别怀疑代码写错试试在代码开头加一句system(chcp 65001);把控制台代码页切换到UTF-8或者调整你的开发环境中文件编码格式。7. 从3个学生到1000个这道题的三种进阶方向7.1 动态学生数量与多门课程题目的“3”是一个固定常数但真实场景里学生数量不可能永远固定。把3改成用变量表示再读入你会发现结构体数组一下子就不好定义了。C语言里有两个解法如果编译器支持变长数组可以int n; scanf(%d, n); struct Student students[n];。如果不支持就得用动态内存分配struct Student *students (struct Student*)malloc(n * sizeof(struct Student));用内存分配后循环里的访问方式从students[i].id变成students[i].id语法上不变但别忘了最后要free(students)释放内存。这是从“数组大小写死”到“按需分配”的一次思维跃迁。如果课程数从2门变成m门结构体字段就不能再写score1、score2了而要改成数组double scores[10]然后在结构体里加一个课程数量字段。平均分要么当场算要么在循环里累加所有课程分数再除以课程数。这时的结构体更像一张真正的学生成绩表而不是只有两个固定字段的小纸条。7.2 从求最高分到排序“找最高分”本质是“求第一名”顺着往下想如果把三个学生按平均分从高到低排个序输出的就不再是孤零零一个最高分而是完整排名。排序逻辑以avg字段为比较依据常用的选择排序写法for (i 0; i 3; i) { int maxIdx i; for (int j i 1; j 3; j) { if (students[j].avg students[maxIdx].avg) { maxIdx j; } } struct Student temp students[i]; students[i] students[maxIdx]; students[maxIdx] temp; }注意这里交换的是整个结构体不是单个字段。这正是结构体的一大优势一个赋值语句students[i] students[maxIdx]就完成了学号、姓名、两门课成绩、平均分的整体交换干净利落。要是用一堆散变量存储学生数据交换姓名要strcpy交换成绩要写三行麻烦好几倍。排序完再输出你会发现所谓“最高分”只是排序结果的第一个元素代码反而更好理解了。这就是数据结构设计带来的连锁收益。7.3 把键盘输入换成文件输入最后一种进阶方向最贴近真实业务数据不是手动敲的而是从文件批量读入。把scanf换成fscanf把printf换成fprintf程序就能从文本文件读学生数据把结果写进另一个文件。FILE *fp fopen(students.txt, r); if (fp NULL) { printf(文件打开失败\n); return 1; } while (fscanf(fp, %d %s %lf %lf, students[i].id, students[i].name, students[i].score1, students[i].score2) 4) { students[i].avg (students[i].score1 students[i].score2) / 2.0; i; } fclose(fp);这一步从“交互式程序”跨到了“批处理程序”也是很多真实系统处理数据的标准模式。题目还是那道题目但因为数据来源变了程序的适用场景就完全变了。我个人在实际操作中的一个体会是写这类结构体程序最容易出错的往往不是语法而是“数据在哪、下一步要取哪个字段”。所以我的习惯是先花一分钟把数据结构画出来再写流程。每次录用完数据先手动算一遍预期结果再去跑代码核对。这个方法笨但比对着屏幕猜半天有用得多。最后再分享一个小技巧如果发现平均分全是整数、最高分老是选不对优先检查除法运算符两边有没有小数参与以及结构体数组下标有没有越界。这两个地方是这道题90%的bug源头排查掉它们你的程序基本就稳了。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑