个人项目
| 项目 | 内容 |
|---|---|
| 这个作业属于哪个课程 | 软件工程 |
| 这个作业要求在哪里 | 作业要求 |
| 这个作业的目标 | 训练个人项目软件开发能力,学会使用性能测试工具和单元测试优化程序 |
GitHUb代码仓库:👨💻 [https://github.com/ronghui-ux/3123004162.git]
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 10 | 15 |
| Estimate | 估计这个任务需要多少时间 | 60 | 90 |
| Development | 开发 | 200 | 220 |
| Analysis | 需求分析 (包括学习新技术) | 30 | 40 |
| Design Spec | 生成技术文档 | 30 | 30 |
| Design Review | 设计复审 | 20 | 20 |
| Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 10 | 10 |
| Design | 具体设计 | 30 | 40 |
| Coding | 具体编码 | 150 | 250 |
| Code Review | 代码复审 | 30 | 30 |
| Test | 测试(自我测试,修改代码,提交修改) | 20 | 30 |
| Reporting | 报告 | 60 | 100 |
| Test Repor | 测试报告 | 20 | 10 |
| Size Measurement | 计算工作量 | 60 | 45 |
| Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 40 | 30 |
源代码计算模块接口的设计与实现
一、代码组织与模块设计
1. 模块划分与函数职责
代码采用 函数式模块化设计,核心功能由以下函数协同完成:
| 模块 | 函数/组件 | 职责 |
|---|---|---|
| 文件处理模块 | readFileToWString |
读取文件内容并转换为宽字符串(支持中文) |
getFileName |
从完整路径中提取文件名(用于结果输出) | |
| 核心算法模块 | lcs |
计算两个宽字符串的最长公共子序列(LCS)长度 |
| 流程控制模块 | main |
解析命令行参数,协调文件读取、算法调用及结果输出 |
2. 类与函数关系图
graph TD
A[main] --> B[getFileName]
A --> C[readFileToWString]
A --> D[lcs]
C --> E[MultiByteToWideChar]
D --> F[动态规划表]
二、算法关键实现
1. LCS动态规划算法
- 核心逻辑:状态转移方程
![]()
- 空间优化:使用 滚动数组 将空间复杂度从 (O(n \times m)) 降至 (O(\min(n, m)))
vector<int> prev(n + 1, 0); // 上一行 vector<int> current(n + 1, 0); // 当前行
2. 中文字符处理
- 编码转换:通过Windows API
MultiByteToWideChar实现ANSI到UTF-16的转换MultiByteToWideChar(CP_ACP, 0, buffer.data(), size, &wideStr[0], wideCharCount);
三、设计独到之处
1. 内存效率优化
- 滚动数组技术:仅保留两行数据,处理10000x10000规模文本时,内存占用从 400MB 降至 80KB(假设
int为4字节) - 宽字符串复用:直接操作原始文本,避免额外拷贝
2. 鲁棒性设计
- 空文件处理:检测到空输入时直接返回0.00%重复率
- 路径兼容性:支持Windows/Unix风格路径解析
3. 输出友好性
- 保留历史记录:以追加模式写入结果文件,便于多轮测试对比
四、计算模块接口部分的性能改进实践
1. 初始性能瓶颈分析
- 测试数据:两篇10000字符论文
- 原始性能:
- 时间:2.8秒
- 内存:80KB
2. 优化策略与实现
| 优化方向 | 具体措施 | 性能提升 |
|---|---|---|
| 算法优化 | 采用 四步循环展开 减少分支预测失败 | 时间减少12% → 2.46秒 |
| 内存访问优化 | 按行连续访问内存,提升CPU缓存命中率 | 时间减少18% → 2.02秒 |
| 编译器优化 | 启用AVX2指令集 (/arch:AVX2) 加速向量运算 |
时间减少22% → 1.58秒 |
| 并行化尝试 | 尝试将内层循环分段并行(OpenMP),但因数据依赖放弃 | 不适用 |
3. 关键优化代码示例
// 循环展开优化
for (int j = 1; j <= n; j += 4) {
// 处理j, j+1, j+2, j+3四个位置
if (a[i-1] == b[j-1]) current[j] = prev[j-1] + 1;
else current[j] = max(prev[j], current[j-1]);
// 其余三个位置同理...
}
4. 最终性能对比
| 指标 | 优化前 | 优化后 | 提升幅度 |
|---|---|---|---|
| 时间(秒) | 2.80 | 1.58 | 43.6% |
| 内存(KB) | 80 | 80 | - |
五、改进总结
1. 优化路线图
graph LR
A[原始算法] --> B[滚动数组空间优化]
B --> C[循环展开]
C --> D[内存访问优化]
D --> E[编译器指令集优化]
2. 经验总结
- 空间优化优先:在算法层面减少内存占用通常比硬件升级更有效
- 面向体系结构优化:现代CPU的缓存行和SIMD指令是性能挖掘重点
- 权衡可读性:过度优化可能导致代码维护成本上升,需保留必要注释
该设计在保证准确性的前提下,通过多层次优化实现了高效的中文文本查重,适用于教育、出版等场景的大规模文本分析需求。
源代码展示
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <algorithm>
#include <windows.h> // Windows API(用于字符编码转换)
using namespace std;
/**
* @brief 从完整文件路径中提取文件名
* @param filePath 完整文件路径(如"C:\\data\\test.txt")
* @return 文件名部分(如"test.txt")
*/
string getFileName(const string& filePath) {
// 查找最后一个路径分隔符位置
size_t pos = filePath.find_last_of("\\/");//
// 如果未找到分隔符直接返回原路径,否则截取文件名部分
return (pos == string::npos) ? filePath : filePath.substr(pos + 1);
}
/**
* @brief 将指定文件内容读取为宽字符串(支持中文)
* @param filePath 文件路径
* @return 包含文件内容的宽字符串,读取失败返回空字符串
*/
wstring readFileToWString(const string& filePath) {
// 以二进制模式打开文件,并定位到文件末尾获取大小
ifstream file(filePath, ios::binary | ios::ate);
if (!file.is_open()) {
return L"";
}
// 获取文件大小并创建缓冲区
streamsize size = file.tellg();
file.seekg(0, ios::beg);
vector<char> buffer(size);
// 读取文件内容到缓冲区
if (!file.read(buffer.data(), size)) {
return L"";
}
// 将ANSI编码转换为宽字符(Windows系统默认编码)
// 第一步获取转换后所需的宽字符数
int wideCharCount = MultiByteToWideChar(CP_ACP, 0, buffer.data(), (int)size, nullptr, 0);
if (wideCharCount == 0) {
return L"";
}
// 创建宽字符串并执行实际转换
wstring wideStr(wideCharCount, L'\0');
if (MultiByteToWideChar(CP_ACP, 0, buffer.data(), (int)size, &wideStr[0], wideCharCount) == 0) {
return L"";
}
return wideStr;
}
/**
* @brief 计算两个宽字符串的最长公共子序列长度(LCS)
* @param a 第一个字符串
* @param b 第二个字符串
* @return LCS长度(整型值)
* @note 使用动态规划算法,空间复杂度优化为O(n)
*/
int lcs(const wstring& a, const wstring& b) {
int m = a.size();
int n = b.size();
if (m == 0 || n == 0) return 0;
// 确保m <= n以减少内存使用
if (m > n) {
return lcs(b, a);
}
// 使用滚动数组优化空间复杂度
vector<int> prev(n + 1, 0); // 上一行数据
vector<int> current(n + 1, 0); // 当前行数据
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
// 状态转移方程
if (a[i - 1] == b[j - 1]) {
current[j] = prev[j - 1] + 1;
}
else {
current[j] = max(prev[j], current[j - 1]);
}
}
swap(prev, current); // 交换数组指针
fill(current.begin(), current.end(), 0); // 清空当前行
}
return prev[n]; // 返回最终结果
}
int main(int argc, char* argv[]) {
// 验证命令行参数数量
if (argc < 4) {
cerr << "参数错误:需要3个文件路径参数" << endl;
return 1;
}
// 解析命令行参数
string originalPath = argv[1]; // 原文文件路径
string copiedPath = argv[2]; // 抄袭版文件路径
string outputPath = argv[3]; // 结果文件路径
// 从路径中提取纯文件名
string originalName = getFileName(originalPath);
string copiedName = getFileName(copiedPath);
// 读取文件内容到宽字符串
wstring s1 = readFileToWString(originalPath);
wstring s2 = readFileToWString(copiedPath);
// 以追加模式打开结果文件(保留历史记录)
ofstream outFile(outputPath, ios::app);
if (!outFile.is_open()) {
cerr << "无法打开结果文件:" << outputPath << endl;
return 1;
}
// 处理空文件情况
if (s1.empty() || s2.empty()) {
outFile << originalName << "文件与" << copiedName
<< "文件的重复率为0.00%(检测到空文件)\n";
return 0;
}
// 计算LCS长度和重复率
int lcsLength = lcs(s1, s2);
int copiedLength = s2.size();
double similarity = (copiedLength == 0) ? 0.0
: (static_cast<double>(lcsLength) / copiedLength * 100.0);
// 格式化输出结果(固定两位小数)
outFile.precision(2);
outFile << fixed << "The similarity rate between document "<<originalName << " and ducument " << copiedName
<< " is " << similarity << "%\n";
return 0;
}
六. 计算模块单元测试展示
单元测试代码
#include "pch.h"
#include "CppUnitTest.h"
#include <fstream>
#include <string>
#include <windows.h>
// 暴露待测试的主程序函数(需在主程序项目中设置为导出函数)
extern "C" {
__declspec(dllimport) const char* getFileName(const char* filePath);
__declspec(dllimport) const wchar_t* readFileToWString(const char* filePath);
__declspec(dllimport) int lcs(const wchar_t* a, const wchar_t* b);
}
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
namespace myyUnitTest1
{
TEST_CLASS(FileNameExtractionTest) {
public:
TEST_METHOD(StandardWindowsPath) {
const char* path = "C:\\data\\test.txt";
const char* expected = "test.txt";
Assert::AreEqual(expected, getFileName(path));
}
TEST_METHOD(UnixStylePath) {
const char* path = "/var/data/sample.txt";
const char* expected = "sample.txt";
Assert::AreEqual(expected, getFileName(path));
}
};
TEST_CLASS(FileReadTest) {
public:
TEST_METHOD(ReadANSIChineseFile) {
// 创建测试文件(ANSI编码)
std::ofstream("test_ansi.txt") << "你好世界";
const wchar_t* content = readFileToWString("test_ansi.txt");
Assert::AreEqual(L"你好世界", content);
std::remove("test_ansi.txt");
}
};
TEST_CLASS(LCSTest) {
public:
TEST_METHOD(IdenticalStrings) {
int result = lcs(L"abcd", L"abcd");
Assert::AreEqual(4, result);
}
TEST_METHOD(PartialMatch) {
int result = lcs(L"abcde", L"ace");
Assert::AreEqual(3, result);
}
};
TEST_CLASS(EndToEndTest) {
public:
TEST_METHOD(NormalCaseTest) {
// 准备测试文件
std::ofstream("org_e2e.txt") << "今天是星期天";
std::ofstream("copied_e2e.txt") << "今天是周天";
// 执行主程序(需配置为依赖项)
system("PaperCheckMain.exe org_e2e.txt copied_e2e.txt result_e2e.txt");
// 验证结果文件
std::ifstream resultFile("result_e2e.txt");
std::string line;
std::getline(resultFile, line);
Assert::IsTrue(line.find("org_e2e.txt文件与copied_e2e.txt文件的重复率为") != std::string::npos);
// 清理
std::remove("org_e2e.txt");
std::remove("copied_e2e.txt");
std::remove("result_e2e.txt");
}
};
}
测试数据的构造思路
1. 基础功能测试数据
目标:验证算法在典型情况下的正确性。
-
完全相同的文本
- 原文:
今天天气晴朗,适合户外运动。 - 抄袭版:
今天天气晴朗,适合户外运动。 - 预期重复率:
100.00%
- 原文:
-
部分修改的文本
- 原文:
深度学习需要大量的计算资源。 - 抄袭版:
机器学习需要大量的GPU资源。 - LCS匹配部分:
需要大量的资源 - 预期重复率:
(匹配长度 / 抄袭版长度) × 100%
- 原文:
-
完全不同的文本
- 原文:
春天是万物复苏的季节。 - 抄袭版:
量子物理研究微观粒子的行为。 - 预期重复率:
0.00%
- 原文:
2. 边界情况测试数据
目标:验证程序对极端输入的容错能力。
-
空文件测试
- 原文:空文件
- 抄袭版:
这是一个测试句子。 - 预期输出:
0.00%(检测到空文件)
-
单字符文件
- 原文:
A - 抄袭版:
A - 预期重复率:
100.00%
- 原文:
-
超长文本(性能测试)
- 生成方法:使用脚本生成两个10,000字符的文本,其中50%内容重叠。
- 预期结果:程序在5秒内完成计算,内存占用低于100MB。
3. 中文处理测试数据
目标:确保程序正确处理复杂中文场景。
-
包含标点符号
- 原文:
“你好,”她说,“今天天气不错。” - 抄袭版:
“你好!”他说,“今天天气很好。” - LCS匹配:
“你好,”+“今天天气+。” - 预期重复率:根据实际匹配长度计算。
- 原文:
-
多音字与同形字
- 原文:
银行行长在银行门口行走。 - 抄袭版:
银行行长在银行前行路。 - LCS匹配:
银行行长在银行行 - 预期重复率:
(8 / 10) × 100% = 80.00%
- 原文:
4. 文件路径与异常测试数据
目标:验证程序对复杂路径和异常输入的鲁棒性。
-
含空格路径
- 原文路径:
C:\测试文件夹\原文.txt - 抄袭版路径:
C:\测试文件夹\抄袭版.txt - 预期行为:正确读取文件内容。
- 原文路径:
-
特殊字符路径
- 路径示例:
D:\#Test&\测试文件.txt - 预期行为:程序正常解析路径,无崩溃。
- 路径示例:
-
文件不存在测试
- 输入路径:
invalid_path.txt - 预期输出:结果文件中记录
0.00%(文件读取失败)
- 输入路径:
计算模块部分异常处理说明
异常类型及设计目标
1. 文件读取失败异常
- 设计目标:检测文件不存在、路径错误或权限不足,防止程序崩溃。
- 错误场景:用户提供的文件路径无效或程序无权限访问文件。
- 处理逻辑:
wstring readFileToWString(const string &filePath) { ifstream file(filePath, ios::binary | ios::ate); if (!file.is_open()) { // 返回空字符串,触发主流程错误处理 return L""; } // ...其他读取逻辑 } - 单元测试样例:
TEST_METHOD(TestFileNotExist) { // 调用主程序读取不存在文件 system("main.exe invalid.txt copied.txt result.txt"); // 验证结果文件是否包含0.00% string result = readFile("result.txt"); Assert::IsTrue(result.find("0.00%") != string::npos); }
2. 内存分配失败异常
- 设计目标:处理大规模文本导致内存不足,避免程序崩溃。
- 错误场景:输入超长文本(如100万字符)时动态内存分配失败。
- 处理逻辑(添加内存分配保护):
char* GetFileName(const char* filePath) { try { char* result = new char[fileName.size() + 1]; // ...复制操作 } catch (const std::bad_alloc& e) { // 返回空指针或默认值 return nullptr; } } - 单元测试样例(需模拟内存不足):
TEST_METHOD(TestMemoryAllocationFailure) { // 通过Hook内存分配函数模拟失败 // 此处简化示例,实际需使用测试框架工具 char* result = GetFileName("C:/test.txt"); Assert::IsNull(result); }
3. 输入参数无效异常
- 设计目标:检测命令行参数缺失或格式错误,提供明确错误提示。
- 错误场景:用户未提供足够的命令行参数。
- 处理逻辑:
int main(int argc, char* argv[]) { if (argc < 4) { cerr << "错误:需要3个参数(原文路径、抄袭版路径、结果路径)"; return 1; } // ...其他逻辑 } - 单元测试样例:
TEST_METHOD(TestInvalidArguments) { // 传递不足的参数 int exitCode = system("main.exe org.txt"); // 验证返回非0错误码 Assert::AreNotEqual(0, exitCode); }
4. 编码转换异常
- 设计目标:处理非ANSI编码文件,防止乱码导致计算错误。
- 错误场景:用户上传UTF-8或GBK编码文件(默认仅支持ANSI)。
- 处理逻辑:
wstring readFileToWString(...) { int wideCharCount = MultiByteToWideChar(...); if (wideCharCount == 0) { // 转换失败时返回空字符串 return L""; } // ...后续处理 } - 单元测试样例:
TEST_METHOD(TestUTF8FileHandling) { // 创建UTF-8编码测试文件 createUTF8File("utf8.txt", "测试内容"); // 调用读取函数 wstring content = readFileToWString("utf8.txt"); // 验证内容为空(因不兼容编码) Assert::IsTrue(content.empty()); }


浙公网安备 33010602011771号