第一次个人编程作业
https://github.com/gitfirehappy/3124004240
| 项目 | 内容 |
|---|---|
| 课程 | 软件工程 |
| 作业要求 | 第一次个人编程作业 |
| 作业目标 | 使用 C++ 完成论文查重程序,并完成性能分析、单元测试和异常处理 |
第一次个人编程作业
一、PSP 预估
以下时间在编码前估计,单位为分钟。阶段合计只统计其子项,最终合计为 Planning、Development 和 Reporting 三个阶段之和。
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) |
|---|---|---|
| Planning | 计划 | 30 |
| · Estimate | · 估计这个任务需要多少时间 | 30 |
| Development | 开发 | 255 |
| · Analysis | · 需求分析(包括学习新技术) | 40 |
| · Design Spec | · 生成设计文档 | 20 |
| · Design Review | · 设计复审 | 10 |
| · Coding Standard | · 代码规范 | 15 |
| · Design | · 具体设计 | 30 |
| · Coding | · 具体编码 | 80 |
| · Code Review | · 代码复审 | 20 |
| · Test | · 测试(自我测试、修改代码、提交修改) | 40 |
| Reporting | 报告 | 55 |
| · Test Report | · 测试报告 | 25 |
| · Size Measurement | · 计算工作量 | 10 |
| · Postmortem & Process Improvement Plan | · 事后总结并提出过程改进计划 | 20 |
| 合计 | 340 |
二、计算模块接口的设计与实现
2.1 项目结构
3124004240/
├─ app/
│ └─ main.cpp 程序入口
├─ src/
│ ├─ CheckError.h 异常类型
│ ├─ FileIO.h/.cpp 文件读写
│ ├─ TextNormalize.h/.cpp UTF-8 解码和文本清理
│ ├─ NGram.h/.cpp 字符 n-gram 词频
│ ├─ Similarity.h/.cpp 余弦相似度
│ └─ CheckPaper.h/.cpp 查重流程
├─ test/
│ ├─ tests.h 测试数据和断言
│ └─ vs_tests.cpp VS 单元测试入口
├─ testdata/ 测试文本
└─ PaperCheck.sln VS 解决方案
生产代码包含 1 个自定义异常类、10 个可调用函数和 1 个文件内辅助函数。没有为简单流程增加业务类。
| 模块 | 类或接口函数 | 作用 |
|---|---|---|
CheckError |
CheckError |
表示参数或文件操作错误 |
FileIO |
readAll、writeAnswer |
读取输入文件;将结果写成两位小数 |
TextNormalize |
normalize |
解码 UTF-8;保留 ASCII 字母数字、基本汉字和 CJK 扩展 A |
NGram |
buildNGram |
统计字符 1-gram 和 2-gram 词频 |
Similarity |
cosineSimilarity |
计算两个词频向量的余弦相似度 |
CheckPaper |
similarityOf、checkFiles、checkToFile、runCheck |
连接参数、文件、算法和输出 |
main |
main |
捕获异常并返回进程状态 |
keepChar 是 TextNormalize.cpp 内部使用的辅助函数,不属于模块对外接口。
2.2 调用关系
main
└─ runCheck 检查命令行参数
└─ checkToFile
├─ checkFiles
│ ├─ readAll × 2
│ └─ similarityOf
│ ├─ normalize × 2
│ ├─ buildNGram × 2
│ └─ cosineSimilarity
└─ writeAnswer
核心函数 similarityOf 的流程:
接收两段已经读入的文本
↓
UTF-8 解码并去掉空白、标点
↓
两边都空?──是──→ 1.00
│否
只有一边空?──是──→ 0.00
│否
构造 1-gram、2-gram 词频
↓
计算余弦相似度并返回
2.3 算法
程序不使用外部分词库。文本经规范化后,分别统计单字符(1-gram)和相邻双字符(2-gram)的出现次数:
- 1-gram 保留整体用字特征,对少量增删改较稳定。
- 2-gram 保留局部顺序,避免只比较单字集合。
两类特征存入 unordered_map<uint64_t, uint32_t>。键表示特征,值表示出现次数。2-gram 的键设置最高位,以免和 1-gram 的键重复。计算余弦时再把词频转换为 double:
similarity = dot(A, B) / (length(A) * length(B))
结果限制在 [0, 1],写入答案文件时保留两位小数。使用哈希表时,算法平均时间复杂度为 O(n + m),空间复杂度为 O(n + m)。
该实现的特点是纯 C++、不依赖词典,并且只访问命令行给出的两个输入文件和一个答案文件。工程使用 /W4 和 Code Analysis 生成,当前没有编译警告。
2.4 运行结果
按作业要求从命令行传入原文、抄袭稿和答案文件的绝对路径。这里使用同一篇长原文,分别与加字版和删字版比较:
main.exe C:\...\orig.txt C:\...\orig_0.8_add.txt C:\...\answer_add.txt
main.exe C:\...\orig.txt C:\...\orig_0.8_del.txt C:\...\answer_del.txt
本机答案文件输出分别为 0.99 和 0.99,且文件中只有一个保留两位小数的浮点数。

三、计算模块的性能改进
性能改进用时:20 分钟。
使用相同的 orig.txt,在 VS Release x64 下重复构造 500 次特征向量。两种实现各交替运行 9 次,分别取中位数。buildNGram 是计算模块中耗时最大的函数。进一步检查发现,词频只需要保存非负整数,首版使用 double 没有必要。
改进内容:把词频类型由 double 改为 uint32_t,只在计算余弦时转换成 double。算法和输出结果不变。
using NGramVec = std::unordered_map<uint64_t, uint32_t>;
同一组测试的结果:
| 项目 | 改进前 | 改进后 |
|---|---|---|
| 词频类型 | double |
uint32_t |
500 次 buildNGram 中位耗时 |
165.632 ms | 161.732 ms |
| 耗时减少 | - | 约 2.4% |
| 耗时最大的函数 | buildNGram |
buildNGram |
这次改进幅度不大,但类型更符合“次数”的含义,也减少了构造词频时的浮点操作。哈希表仍会预留约 2n 个位置,避免反复扩容。
性能截图启用 PERF_PROFILE 宏:输入文件只读一次,随后重复计算 3000 次,最后只写一次答案。这只用于让 VS CPU 采样有足够持续时间;提交前取消该宏,正常版本仍只计算一次。

四、计算模块的单元测试
使用 Visual Studio C++ 单元测试框架,共编写 15 个测试。测试按代码分支设计,覆盖正常输入、边界输入和异常输入。
| 测试目标 | 测试用例 | 构造思路 |
|---|---|---|
| 相同文本 | IdenticalTexts |
两个输入完全相同,期望为 1.00 |
| 题目示例 | ExampleSentence |
使用题目给出的两句话,结果应大于 0 且小于 1 |
| 两边为空 | BothEmpty |
空串和只有标点的文本,期望为 1.00 |
| 一边为空 | OneEmpty |
分别让原文和抄袭稿为空,期望为 0.00 |
| 忽略标点 | PunctuationIgnored |
你好,世界! 与 你好世界,期望相同 |
| 无关文本 | UnrelatedTexts |
使用没有公共字符的两个字符串 |
| 增加字符 | AddNoise |
在原句中插入若干汉字 |
| 删除字符 | DeleteChars |
从原句中删除若干汉字 |
| 替换字符 | ReplaceChars |
将原句中的一个汉字替换 |
| UTF-8 清理 | NormalizeKeepsCjk |
混合汉字、字母、数字和标点 |
| 文件不存在 | FileNotFound |
使用确定不存在的输入路径 |
| 参数错误 | ArgcError |
令 argc != 4 |
| 输出失败 | WriteFail |
将目录 . 当作答案文件路径 |
| 输出格式 | WriteFormat |
写入 0.634,检查结果为 0.63 |
| 完整流程 | CheckToFile |
使用两个相同临时文件,检查答案文件 |
部分测试代码:
inline void test_both_empty() {
expectNear(similarityOf("", ""), 1.0, 1e-9,
"两个空文本视为完全相同");
expectNear(similarityOf(",。!", " "), 1.0, 1e-9,
"去掉标点后都空,视为相同");
}
inline void test_replace_chars() {
const std::string orig = "今天晚上我要去看电影";
const std::string plag = "今天晚上我要去看电映";
expectTrue(similarityOf(orig, plag) > 0.75,
"少量替换后仍应很相似");
}
本机测试结果:15 个测试全部通过。

测试覆盖了查重主流程、两个空文本分支、单边空文本分支、参数错误以及文件打开和写入失败。隐藏评测数据没有公开,因此测试只验证程序接口和可明确判断的行为。
五、计算模块的异常处理
程序定义 CheckError 表示可预期错误。main 捕获 CheckError 和其它 std::exception,向标准错误输出原因并返回 1,避免未捕获异常导致程序崩溃。
5.1 参数数量错误
设计目标:必须接收原文、抄袭稿和答案文件三个路径。参数数量不等于 3 时停止执行,不读取文件。
对应场景:直接运行 main.exe,或漏传任意路径。
对应测试:
inline void test_argc_error() {
bool thrown = false;
char* args[] = {const_cast<char*>("main.exe")};
try {
runCheck(1, args);
} catch (const CheckError&) {
thrown = true;
}
expectTrue(thrown, "参数个数不对应抛 CheckError");
}
5.2 输入文件无法打开
设计目标:原文或抄袭稿不存在、无权限读取时,指出打不开的路径并停止计算。
对应场景:输入路径不存在。
对应测试:
inline void test_file_not_found() {
bool thrown = false;
try {
readAll("__no_such_paper_check_file__.txt");
} catch (const CheckError&) {
thrown = true;
}
expectTrue(thrown, "缺文件应抛 CheckError");
}
5.3 答案文件无法写入
设计目标:答案路径是目录或没有写权限时,报告写入错误,不能假装执行成功。
对应场景:把当前目录 . 当作答案文件路径。
对应测试:
inline void test_write_fail() {
bool thrown = false;
try {
writeAnswer(".", 0.50);
} catch (const CheckError&) {
thrown = true;
}
expectTrue(thrown, "答案路径不可写应抛 CheckError");
}
六、PSP 实际耗时
以下实际耗时合计为 180 分钟。阶段合计只统计其子项。
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 30 | 10 |
| · Estimate | · 估计这个任务需要多少时间 | 30 | 10 |
| Development | 开发 | 255 | 130 |
| · Analysis | · 需求分析(包括学习新技术) | 40 | 15 |
| · Design Spec | · 生成设计文档 | 20 | 10 |
| · Design Review | · 设计复审 | 10 | 5 |
| · Coding Standard | · 代码规范 | 15 | 5 |
| · Design | · 具体设计 | 30 | 15 |
| · Coding | · 具体编码 | 80 | 45 |
| · Code Review | · 代码复审 | 20 | 10 |
| · Test | · 测试(自我测试、修改代码、提交修改) | 40 | 25 |
| Reporting | 报告 | 55 | 40 |
| · Test Report | · 测试报告 | 25 | 15 |
| · Size Measurement | · 计算工作量 | 10 | 5 |
| · Postmortem & Process Improvement Plan | · 事后总结并提出过程改进计划 | 20 | 20 |
| 合计 | 340 | 180 |
浙公网安备 33010602011771号