个人项目

项目 内容
这个作业属于哪个课程 软件工程
这个作业要求在哪里 作业要求
这个作业的目标 训练个人项目软件开发能力,学会使用性能测试工具和单元测试优化程序

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());
    }
    

posted @ 2025-03-08 23:36  辉辉辉——  阅读(35)  评论(0)    收藏  举报