简介:面向高校C++课程分组训练的一份实验资源包,适合作业参考、项目复盘与团队协作练习。资源围绕BJTU程序设计分组训练实验三展开,内含完整可编译的C++工程源码与配套实验报告,覆盖基本语法、面向对象设计、数据结构运用、文件读写、模板及异常处理等核心知识点;通过.cpp源文件与.h头文件的分工,清晰展示模块化封装与接口声明方式,有助于理解大型程序的拆分与组织。实验报告部分详细记录关键步骤与问题解决思路,可借鉴其条理结构与分析方式;同时代码注释与命名规范也提供了提升可读性和可维护性的范例。包体共31个文件,以.cpp、.h、.txt、.docx为主,附带构建过程产生的obj、log、pdb、ilk等中间文件,整体压缩包仅2.99MB,便于快速下载与解压使用。目前已有1273人学习,适合正在完成类似分组编程任务的高校学生,以及希望通过完整实例提升C++工程能力的开发者。
1. 实验三不是"写个程序",是逼你把字符串、数组和日期当成工程件
很多人拿到"BJTU程序设计分组训练实验三"这个题目,第一反应是找现成源码,第二反应是赶紧把功能跑通。但真正扣分的地方往往不在功能,而在三个隐蔽的细节:字符串比较用了==、数组越界没拦截、日期格式解析抛异常直接崩。这个实验通常要求你用 Java 写一个小型信息管理系统,把字符串、数组、日期类三样东西串起来用,表面是业务逻辑,实际是考察你对这三类基础 API 的边界条件是否敏感。这篇文章不提供任何"参考答案",只讲一套我帮人改作业时见过的、最稳的做法:理论怎么理解、代码怎么写、参数怎么设、坑在哪。适合正在做这个实验的本科生,也适合带实验的助教用来设计验收点。
2. 实验三的底层理论:字符串不可变、数组边界与日期类选型
2.1 字符串你用==比较,实验三就凉了一半
Java 字符串的不可变性(immutability)不是一句口号,它直接影响你怎么写判定逻辑。String底层是final char[],每次修改都会生成新对象,所以两个内容相同的字符串在堆里可能是两个不同对象。用==比较的是引用地址,而不是内容。实验三里最常见的场景是用户输入一个命令"add"或"delete",你用command == "add"去判断,输入是通过Scanner读进来的,运行时必然走new String,引用地址不一致,判断永远为false。
正确做法是用equals或equalsIgnoreCase。我一般会要求代码里禁止出现对字符串变量使用==,除非你在比较两个明确指向同一个 interned 字符串的字面量。更稳的方案是把命令先trim()掉首尾空格再比较,否则用户多敲一个空格,equals也会失败。另一个容易被忽略的点是switch语句在 Java 7 之后支持字符串,它内部调用的是equals,但如果你需要忽略大小写,switch就帮不上忙,得先统一toLowerCase()。
字符串拼接也会影响性能。实验三如果在一个循环里用+拼接 1000 次,比如生成报表,每次拼接都会创建新的StringBuilder和String,耗时是线性增长的。正确做法是在循环外用StringBuilder,循环内只append。这一点不一定会被性能测试抓到,但代码审查时老师一眼就能看出来。所以理论部分要记住:不可变是安全性的代价,安全意味着你必须用方法而不是操作符去比较内容。
2.2 数组长度固定,实验三要用"逻辑删除"或"紧凑存储"
数组在 Java 里是定长容器,创建后长度不可变。实验三如果用数组存学生记录,你会立刻遇到"添加满了怎么办"和"删除后中间有空位怎么办"两个问题。很多人的第一反应是"扩容量",手写一个Arrays.copyOf去扩容,这当然能解决问题,但会让代码变得很难看。更常见的设计是:数组只放固定数量的记录,比如 100 条,超出时提示"存储已满";删除时不移动元素,而是给每个元素加一个valid布尔字段,只在遍历时跳过valid == false的记录。这种做法叫"逻辑删除"。
逻辑删除的优点是不需要频繁搬移数组元素,删除操作是 O(1),缺点是遍历时要多一个判断,且数组实际占用不减少。另一种做法是"紧凑存储":每当删除一个元素,就把后续元素整体前移一位,并把最后一个位置置null。这样数组始终是紧凑的,遍历简单,但删除是 O(n)。实验三的数据量极小,两种都能用,我倾向于逻辑删除,因为你需要演示"删除后新增还能复用原位置"的能力,这比单纯移动元素更能体现对数组下标的掌控。
还需要考虑数组的类型。如果你用String[]存所有字段,比如arr[0]="张三", arr[1]="2023-09-01",代码耦合度会爆炸。正确做法是定义学生类Student,数组类型为Student[]。类的属性是id、name、birthday(类型用LocalDate)、valid。这样数组操作就是对象引用操作,清晰且不易出错。理论层面要理解:数组的边界是 0 到length-1,任何越界都会抛ArrayIndexOutOfBoundsException,这是实验三运行时崩溃的头号原因,后面第 4 章会详细说排查方法。
2.3 日期类别用Date做业务,LocalDate才是正解
Java 8 之前的日期类是java.util.Date和java.util.Calendar,它们有著名的可变性、线程安全隐患、月份从 0 开始计数的反人类设计。实验三如果你还在用Date存生日,计算年龄时就得自己按年月日拆,还要小心getMonth()返回 0 表示一月,一不留神就少算一个月。正确选择是使用java.time包下的LocalDate。它是不可变类,提供now()、of(int year, int month, int day)、parse(CharSequence text)等工厂方法,还有plusDays、minusMonths、until等时间计算能力。
以"根据生日算年龄"这个实验三标配功能为例,用LocalDate写是这样:
import java.time.LocalDate; import java.time.Period; public int calculateAge(LocalDate birthDate) { LocalDate today = LocalDate.now(); return Period.between(birthDate, today).getYears(); }Period.between会计算两个日期之间的年、月、日差异,getYears()直接拿到整年数,不需要你手动判断当前日期是否过了生日。这个 API 的内部逻辑已经处理了"如果今年生日还没到,年龄减一"的规则。如果你用Date,同样的逻辑至少要多写 5 行代码,而且容易出错。
日期解析也是实验三的高频坑。从控制台读入一个"2023-09-01"格式的字符串,转成LocalDate用LocalDate.parse("2023-09-01")即可,它默认使用ISO_LOCAL_DATE格式。但如果用户输入"2023/09/01"或"20230901",解析就会抛DateTimeParseException。我一般会建议在输入层做校验,用DateTimeFormatter自定义格式,或者干脆提示用户必须按yyyy-MM-dd输入。这个校验逻辑放到第 4 章的踩坑清单里细说。
3. 实验三的代码骨架:一个可复现的学生信息维护程序
3.1 需求假设与模块划分
虽然 BJTU 的实验三具体题目可能每年不同,但绝大多数分组训练都围绕"学生信息管理"或"图书管理"展开,核心操作是增删改查加统计。我以"学生信息维护"为例,假设要求如下:用数组保存最多 50 个学生,每个学生有学号、姓名、生日(日期类型)、成绩;支持添加学生、删除学生、按学号查找、列出所有学生、计算平均年龄或平均成绩。这个假设覆盖了字符串(学号、姓名比较)、数组(存储与遍历)、日期类(生日解析、年龄计算)三个考点,你可以直接映射到自己的题目。
模块划分我建议分成三层:Student实体类、StudentManager业务类、Main交互入口。实体类只负责数据持有;业务类负责数组操作和业务规则;Main负责Scanner读取和打印。这样划分的好处是核心业务不依赖控制台,后面用 JUnit 测试时可以绕过Main直接测StudentManager。很多人喜欢把所有代码写在main方法里,实验三数据量小确实能跑,但答辩时老师一问"删除逻辑在哪",你就得在一堆if里翻,印象分直接掉。
3.2 核心代码:字符串解析与数组操作
下面是StudentManager中最关键的两个方法:添加学生和删除学生。注意我用逻辑删除的方式处理数组元素。
public class StudentManager { private Student[] students; private int count; private static final int MAX_SIZE = 50; public StudentManager() { students = new Student[MAX_SIZE]; count = 0; } public boolean addStudent(Student s) { for (int i = 0; i < students.length; i++) { if (students[i] == null || !students[i].isValid()) { students[i] = s; count++; return true; } } return false; // 数组已满 } public boolean deleteStudent(String studentId) { for (int i = 0; i < students.length; i++) { if (students[i] != null && students[i].isValid() && students[i].getId().equals(studentId)) { students[i].setValid(false); count--; return true; } } return false; } }addStudent遍历数组,找到第一个空位或已被逻辑删除的位置,放入新对象。这里有一个易错点:判断students[i]是否为null必须写在前面,否则调用isValid()会抛空指针异常。deleteStudent用equals比较学号,并检查isValid(),避免重复删除已经无效的记录。count记录有效元素个数,用于判断是否满员。为什么不用null表示删除?因为那样会导致数组中间出现空洞,后续添加时必须从头找,而逻辑删除允许你复用任意位置,并且遍历时只需要检查isValid()一个条件。
查找方法类似,但注意返回值类型。如果是返回Student对象,找不到时返回null;如果是返回数组下标,找不到时返回-1。这两种约定都要在方法注释里写清楚,不然Main里拿返回值会误判。我一般统一用"返回数组下标,-1 表示未找到",因为后续修改和删除都能复用这个下标,避免重复遍历。
3.3 日期计算与控制台交互的参数说明
日期相关的核心操作有两个:解析生日字符串和计算年龄。我在Main中建议这样写:
Scanner scanner = new Scanner(System.in); System.out.print("请输入生日(格式 yyyy-MM-dd):"); String birthStr = scanner.nextLine().trim(); LocalDate birthDate; try { birthDate = LocalDate.parse(birthStr); } catch (DateTimeParseException e) { System.out.println("日期格式错误,请重新输入"); return; } Student s = new Student(id, name, birthDate, score); manager.addStudent(s);trim()去掉用户误输入的前后空格,LocalDate.parse解析失败会抛出DateTimeParseException,我们必须捕获并提示,而不是让程序崩溃。这里有一个参数设计的细节:parse方法默认要求格式严格是yyyy-MM-dd,如果你希望用户输入yyyy/MM/dd,就得换DateTimeFormatter.ofPattern("yyyy/MM/dd")。但实验题目通常没有规定输入格式,我建议直接在提示里写明格式,而不是去兼容多种格式,这样代码更简洁。
年龄计算方法的参数是LocalDate,不是字符串。如果你在Student里存的是String birthDate,那么每次计算年龄都要先解析,浪费性能且容易出现格式不一致。所以存对象时直接转成LocalDate,实体类里就不该出现字符串日期。这是从源头消灭一类 bug 的设计决策。另一个注意点是Period.between得到的是年月日差值,getYears()返回整年,如果题目要求"精确到天"的年龄,你需要自己再计算剩余月数,公式为:总天数除以 365.2425,但这样不精确,一般不会考。
4. 实验三的必调参数和踩坑清单
4.1Scanner的nextLine与nextInt混用问题
这是 Java 控制台程序里最经典的坑。如果你先scanner.nextInt()再scanner.nextLine(),第一次nextLine()会读到残留的换行符,直接返回空字符串,导致后续输入错乱。原因是nextInt()只读取整数,不消费行尾的\n,而nextLine()却以\n为结束标志。我见过太多实验三代码因此出现"跳过一次输入"的问题。
解决办法有两种。一是全部用nextLine()读取整行,再用Integer.parseInt()或Double.parseDouble()转换。这样每次读取都消费换行符,行为统一。二是每次调用nextInt()后立即加一行scanner.nextLine()来吞掉残留换行。第二种方法容易忘,我推荐第一种。例如读学号后读姓名:
String id = scanner.nextLine().trim(); int score = Integer.parseInt(scanner.nextLine().trim());parseInt如果遇到非数字字符串会抛NumberFormatException,所以外面要包try-catch,或者验证正则\d+。不少实验指导书会忽略这个细节,但验收时老师专门会输入"abc"看你的程序是否优雅退出。我一般会写一个readInt辅助方法,内部循环处理,直到用户输入合法数字为止。
4.2SimpleDateFormat线程不安全与DateTimeFormatter
如果你还在用SimpleDateFormat格式化日期,在单线程实验里不会出问题,但这是一个潜在的"隐形雷"——它是可变的、非线程安全的。如果实验三要求你开多线程预处理数据,多个线程共享同一个SimpleDateFormat实例,格式化结果会错乱,甚至抛NumberFormatException。实验三大概率不涉及多线程,但代码审查时老师可能会问"你这个格式化线程安全吗"。
正确做法是使用DateTimeFormatter,它是不可变且线程安全的。它的用法是:
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日"); String formatted = birthDate.format(formatter);DateTimeFormatter的ofPattern每个线程都可以调用,不需要担心共享状态。另一个注意点:LocalDate默认的toString()输出是2023-09-01,如果你要输出中文格式,必须用formatter,不要手动拼字符串,否则拼接时容易漏掉补零,比如 9 月变成"9月"而不是"09月"。DateTimeFormatter默认严格校验月份是 1 到 12,日期是 1 到 31,并且会校验该月实际天数(2 月 30 日会被拒绝),这比手写判断安全得多。
4.3 数组越界与输入长度校验
数组越界通常发生在两个场景:一是遍历到students.length时,条件写成了<=;二是用户在菜单输入了超范围的选项,你用menu[choice]取值时直接越界。第一种是低级错误,代码审查阶段就能避免;第二种需要你对用户输入做范围校验。我一般这样写菜单处理:
int choice = readInt(); if (choice < 1 || choice > 5) { System.out.println("无效选项,请输入 1-5"); continue; }对于学号这类定长字段,我还会校验长度。比如学号是 10 位数字,那么id.length() != 10 || !id.matches("\\d{10}")就拒绝。不要相信用户会按规范输入,这是实验三训练的核心素质之一。数组的初始容量如果设为 50,添加前要检查是否已满:count >= 50时直接提示。这里的count是有效记录数,而不是数组长度,记住这一点能在逻辑上避免很多差一错误。
5. 实验三的进阶玩法:用 JUnit 和边界用例把答辩变成表演
5.1 用 JUnit 4/5 给StudentManager写单元测试
很多实验只要求提交源码和运行截图,但如果你能在答辩现场展示测试代码,绝对加分。StudentManager不依赖控制台,天然适合测试。我习惯用 JUnit 5,测试逻辑删除是否真的复用位置:
import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; public class StudentManagerTest { @Test void testDeleteAndAddReusesPosition() { StudentManager manager = new StudentManager(); Student s1 = new Student("2023000001", "张三", LocalDate.of(2000, 1, 1), 90); Student s2 = new Student("2023000002", "李四", LocalDate.of(2000, 2, 2), 80); manager.addStudent(s1); manager.addStudent(s2); manager.deleteStudent("2023000001"); Student s3 = new Student("2023000003", "王五", LocalDate.of(2000, 3, 3), 70); assertTrue(manager.addStudent(s3)); assertEquals("2023000003", manager.findStudent("2023000003").getId()); } }这个测试验证删除后新添加的学生能占用被释放的位置。assertEquals那行顺便测试了findStudent方法的正确性。再写一个边界测试:删除不存在的学号应该返回false,数组满时添加应该返回false。这些测试覆盖的是业务规则,而不是具体实现,所以即使你重构了存储方式(比如改成ArrayList),测试仍然有效。
5.2 自测脚本与答辩前的检查清单
没有 JUnit 环境时,也可以用纯main方法做冒烟测试。我一般会在Main里加一个test参数,如果启动时带--test就执行一组预设命令,不进入交互循环。这样验收时你可以一键演示所有功能,避免现场打字出错。检查清单如下:
| 测试点 | 输入 | 预期结果 |
|---|---|---|
| 日期格式错误 | 2023/01/01 | 提示格式错误,不崩溃 |
| 删除不存在的学号 | 999 | 返回"未找到" |
| 重复添加相同学号 | 同一学号两次 | 拒绝或覆盖(按需求定) |
| 数组满时添加 | 50 条后继续 | 提示存储已满 |
| 年龄计算边界 | 生日恰好是今天 | 年龄显示正确,未少一岁 |
最后在答辩前用javac -Xlint编译一次,查看所有警告并消除。-Xlint会提示字符串比较、未关闭资源等问题,这些往往是老师快速判断你水平的标准。Scanner记得在main最后关闭,虽然程序结束会自动释放,但养成习惯能加分。把这套流程走完,实验三拿高分不是靠运气。
本文还有配套的精品资源,点击获取