第二次作业
| 这个作业属于哪个课程 | 软件基础课程班级 |
|---|---|
| 这个作业的目标 | 实现英文文本计数程序,通过版本迭代、单元测试和性能分析,练习从需求明确到验证交付的开发过程。 |
| 姓名-学号 | 徐壮-2024329301136 |
从文本计数到代码行分析:一次 C++ 命令行程序的开发记录
项目仓库:text-counter。单纯关于项目具体可以看README,写的很详细了。
这次作业要实现一个命令行文本计数程序,统计英文 txt 文件中的字符、单词和句子,并尝试增加代码行分类。最终使用 C++17、CMake 和 GoogleTest 完成实现,主要在 VS Code 中操作,性能分析使用 Visual Studio 自带的 CPU 使用率工具。
项目使用了 AI 辅助。代码、测试和文档的编写均有 AI 参与,我负责确认需求和技术方案、审核材料、运行程序、截图及提交。

下面结合实际结果,记录这次作业中几个值得展开的问题。
一、写计数程序之前,先确定“怎么算”
“统计单词数”看起来很明确,落实到具体文本时却会出现分歧:don't 算一个还是两个?well-known 怎么处理?文件最后没有句号,要不要算一句?如果这些规则不先说明,即使程序运行成功,也很难判断输出是否正确。
本项目采用以下口径:
| 输入情况 | 处理方式 |
|---|---|
| 空格、标点和换行 | 都计入字符数 |
| Windows 的 CRLF 与 LF 换行 | 都按一个换行字符计数 |
| 文件开头的 UTF-8 BOM | 不计入字符数 |
don't、well-known |
各算一个单词 |
a--b、abc123def |
各算两个单词,纯数字不计为单词 |
Really?! |
算一句,连续结束标点不增加空句 |
| 文件末尾有文字但没有结束标点 | 仍计一句 |
句子统计按 . ! ? 划分,没有加入英文缩写和网址的语义识别。例如 Mr. 中的句点也按结束标点处理。这项规则在文档和测试中保持一致,方便使用者判断程序是否适合自己的文本。
用于核对的样例只有两行:
Hello, world!
How are you? I am fine.
第二行后保留一个换行。两行正文分别是 13、23 个字符,加上两个换行,结果为 38 个字符;单词是 8 个,句子是 3 个。样例足够短,可以人工复核,再与程序结果比较。

二、三个版本分别解决了什么
v0.1:先让项目能够构建和运行
第一个版本只有程序入口和构建配置,运行后输出“空项目已建立”。这个阶段的作用是确认源文件能够经过编译和链接生成 wc.exe,不包含统计功能。
最初电脑上有 VS Code,但没有把编译器和 CMake 配置到同一个可用环境中。检查后使用了已有的小熊猫 GCC 工具链,并补齐 CMake。构建时又遇到旧版 MinGW Make 无法正确处理中文路径的问题,最终通过构建期间临时映射英文盘符解决,文件仍保存在原目录。
这一过程把编辑器、构建工具和编译器的分工区分开了:VS Code 用来编辑和操作项目,CMake 根据配置生成构建规则,编译器负责把 C++ 源码转换为可执行程序。

v0.2:从程序入口扩展到可测试的统计功能
第二个版本加入文件读取、字符统计、单词统计和句子统计,支持如下调用:
wc.exe -c file.txt
wc.exe -w file.txt
wc.exe -s file.txt
wc.exe -a file.txt
实现中没有把全部逻辑放进 main()。命令行入口负责解析选项、打开文件和输出结果,统计模块接收输入流并返回统计结果。这样测试时可以直接提供一小段字符串,不必每个用例都创建文件或启动程序。
三个基础指标在一次扫描中完成。字符经过换行统一处理后计数;单词统计保存是否处于单词内部的状态;句子统计保存当前片段是否出现过字母或数字,基础统计不需要把整篇文本装入内存。
v0.3:在保留基础功能的同时增加行分类
第三个版本增加 -l 选项,输出代码行、空行和注释行。这里最容易出错的地方,是把“出现注释符号”直接等同于“这一行是注释”。
例如:
const char* url = "http://example.com";
int value = 1; // 一行代码后面带注释
/* 块注释
延续到下一行 */
第一行字符串里的 // 不应改变行分类;第二行虽然有注释,仍然属于代码行;块注释则需要把状态保留到下一行。因此,行分类使用状态机,分别处理普通代码、单行注释、块注释、字符串和字符字面量等情况。
测试还覆盖了原始字符串与 1'000 这样的数字分隔符,避免把数字中的撇号误判为字符字面量的开始。
分类规则是:有代码就算代码行,只有注释及空白就算注释行,只含空白就算空行。三类互斥。样例文件共 11 行,结果为 5 个代码行、2 个空行、4 个注释行。

各阶段都保留了对应代码与提交,并通过 v0.1、v0.2、v0.3 标签定位。
三、测试不能只验证样例文件
样例输出正确,只能说明这一份输入通过了。实际测试还需要覆盖空文件、不同换行、无末尾换行、连续标点、长文本、中文路径和错误参数。
最终测试分为两层:
| 层次 | 数量 | 检查内容 |
|---|---|---|
| GoogleTest 单元测试 | 57 个 | 24 个文本统计用例、4 个边界用例、29 个代码行分类用例 |
| 命令行集成测试 | 18 个场景 | 实际启动程序,检查选项、输出、退出码及文件路径 |
在 CTest 中,整组命令行集成测试算作一项,因此完整结果是 58 项 CTest 通过,单独运行 GoogleTest 则显示 57 个测试通过。两张截图上的数量不同,原因在这里。

测试过程中还出现过预期字符数手算错误的情况。这提醒我,测试失败后需要同时核对实现和预期值,不能直接把程序输出填回测试。对于短文本,应逐字确认字符和换行;对于大量重复文本,可以用“重复行数 × 每行计数”独立计算预期结果。
最终的 Debug、Release 测试均通过。提交前还把准备上传的文件复制到一个没有构建缓存的目录,重新配置、编译并运行测试,确认项目不依赖原来的 build 目录。
四、性能测试分别回答两个问题
文件变大后,需要运行多久
耗时测试使用 Release 构建,生成约 1MiB、10MiB、50MiB 的英文文件。每个文件先预热一次,再测量五次,取中位数;计时范围包含读取和统计,不包含文件打开与控制台输出。
2026 年 10 月 30 日的复测结果如下:
| 目标规模 | 实际字符数 | 单词数 | 句子数 | 耗时中位数(毫秒) |
|---|---|---|---|---|
| 1MiB | 1,048,582 | 216,948 | 72,316 | 4.847 |
| 10MiB | 10,485,762 | 2,169,468 | 723,156 | 50.845 |
| 50MiB | 52,428,810 | 10,847,340 | 3,615,780 | 260.156 |
环境为 Windows x64、Intel Core i9-13980HX、GCC 11.4.0。测试文件按完整行生成,所以实际大小略大于目标值。从 10MiB 到 50MiB,数据量约增加到 5 倍,耗时约增加到 5.12 倍,与基础统计单次扫描的实现相符。这些结果受机器负载和文件缓存影响,不是所有电脑上的固定耗时。

CPU 采样主要集中在哪里
为了完成 IDE 性能分析,使用 Visual Studio 自带的 CPU 使用率工具分析 wc_profile。它持续处理固定的内存文本,每轮校验计数结果,便于观察统计路径的 CPU 开销。
截图中,textcount::count 的总 CPU 采样占比为 99.73%,自 CPU 占比为 8.87%。两者的区别是:前者包含下级函数调用,后者只计算函数自身,主要外部调用热点位于 msvcp140.dll。

这里的百分比是采样占比,不是整机 CPU 利用率,也不是毫秒耗时。文件耗时测试与 CPU 分析采用的工作负载不同,分别记录才能避免混淆。
五、这次作业留下的改进方向
这次作业中,我对方案和文档的多次修改主要围绕同一件事:别人拿到项目后,能否知道它怎么算、怎样运行,以及结果依据是什么。代码、测试、截图和文档需要互相对应。后续也需要继续熟悉状态机和构建脚本,能够独立解释关键分支,并在增加规则时自己补上测试。
AI 使用说明
本项目的方案讨论、代码与测试编写、问题排查和文档整理均使用了 AI 辅助。技术栈和需求由我确认,运行截图和仓库提交由我操作,文档经过人工审核与修改。
仓库内的 AI 使用记录。其在v03的docs里。


浙公网安备 33010602011771号