单词计数——软件工程结对项目实记
1. 项目描述
Github地址:WordCount - Github
和队友分析了各个题目以后,结合需求与能力,最终选择了较为简单的第一道题目。
通过一条条的需求分析,本次软工项目需要实现以下功能。
- 统计单一文件的词数、字符数、行数,还需能够显示行的具体信息,如代码行、空行、注释行的数目
- 递归处理目录下所有合法文件的基础统计结果
- 在图形界面显示文件,通过点击文件显示文件具体统计结果,并返回当前目录及所有子目录中代码文件的具体行信息。
本次的代码在实现过程中,通过对困难的攻关,以及前辈的指导下,很多功能的实现,都借助C++自带的函数,通过对文件流和数据流的操作来完成。再次也感谢前辈一直的帮助。
2. PSP 2.1 表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 20 | 30 |
· Estimate | · 估计这个任务需要多少时间 | 20 | 30 |
Development | 开发 | 1470 | 3470 |
· Analysis | · 需求分析(包括学习新技术) | 200 | 300 |
· Design Spec | · 生成设计文档 | 60 | 60 |
· Design Review | · 设计复审(和同事审核设计文档) | 20 | 20 |
· Coding Standard | · 代码规范(为目前的开发制定合适的规范) | 30 | 30 |
· Design | · 具体设计 | 60 | 60 |
· Coding | · 具体编码 | 600 | 900 |
· Code Review | · 代码复审 | 300 | 400 |
· Test | · 测试(自我测试,修改代码,修改提交) | 200 | 300 |
Reproting | 报告 | 170 | 250 |
·Test Report | · 测试报告 | 120 | 200 |
·Size Measurement | · 计算工作量 | 30 | 30 |
·Postmortem & Process Improvement Plan | · 事后总结,并提出改进计划 | 20 | 30 |
合计 | 1660 | 6110 |
3. 确定解题思路
统计功能实现:
关于代码的词数,字符数,以及行数,都可以通过判断读入的字符来加以辨别及计数。
字符数是最简单的,读入不为空即加一。
词数这里有点歧义,因为不确定这里要求的单词是不同的单词,还是只要出现过就需要计数的单词。为了设计简单,采用后种理解。将特殊字符转为空格后,开始读入,当读到字母时,确认为单词,再读入过空格判断单词何时停止并将单词数加一。
涉及到行数就更简单了,只需要通过读入换行符的个数就可以判断行数。
在复杂统计中,需要判断此行为空行还是注释行还是代码行并对应加一,这是只需要判断每行非空字符的前几位就可加以判断。
递归处理功能实现:
设计递归处理模块,每次打开所在文件夹的一个代码文件,进行信息统计并返回统计结果。
图形界面处理实现:
这里在经过查资料学习后,发现实现这一功能需要首先获取Windows系统路径,再通过对选取的文件进行统计。
因此在代码头文件库中加入包含获取系统路径函数的头文件,#include <locale.h>和 #include <ShlObj.h>。
4. 实验具体设计
我们经过分析,根据我们需要的功能,明确简洁的设计出了七个函数,不同的函数对应各自的功能,如下图所示,具体设计将在代码说明中详细介绍。
5.性能分析及优化
- 优化这里需要重点讲以下,按照原来的设想,挨个字符读入,代码可能会有几百行,在后来探索文件处理方法时,偶然发现了一种又快又好的文件流操作方式,对输入流操作:seekg()与tellg(),对输出流操作:seekp()与tellp()。只要弄懂这种操作方式,就可以省去很多行代码,并且通过内置的提前编译过的函数,提升系统性能。
- 此外,cin 搭配上>>流操作符可以从文件缓冲区流里提取数据,由于对>>进行了重载,所以可以提取不同形式的数据。
性能分析工具为Visual Stdio 自带分析工具,以下是各项功能的运行时间及函数使用情况。
可以看出,当调用程序时,首先通过main函数调用具体功能实现函数,不同的功能调用情况下,除main函数外,各函数使用情况相差较大,具体可见以下各图。
① -a
② -c
③ -l
④ -s⑤ -w
⑥ -x
6.代码说明
- 首先确定程序整体框架,定义所需要使用的各个函数,以及重要变量,具体的函数和变量功能写在了注释中,可以清楚明了的看到它们的作用。
3. 具体功能函数的解释:七个函数的简单说明
①函数 void open_file(char *filename)
:这个函数比较简单,基本操作打开文件。
②函数int count_of_character(char *filename)
:这个函数的作用是统计字符个数,通过调用C++自带的tellg()
函数,直接获得文件的字符元数。
③函数int count_of_character(char *filename)
:这个函数的作用是统计文件中的单词数,cin 搭配上>>流操作符 就是从缓冲区流里提取数据,由于对>>进行了重载,所以可以提取不同形式的数据。本函数中通过对字符串的读入来判断单词数量。
④函数int count_of_line(char *filename,bool print)
:这个函数的作用是统计文件中的行数,通过逐行读取实现行数增加。
⑤函数void count_of_more(char *filename,int *c,int *n,int *e)
:这个函数的作用是分别统计空白行,代码行,注释行。通过对读入的判断,不同情况下的行分到不同的类目中去。
void count_of_more(char *filename,int *c,int *n,int *e){
int empty_line = 0; // 空白行
int code_line = 0; // 代码行
int note_line = 0; // 注释行
char buffer[1024];
ifstream file(filename);
while (!file.eof()){
memset(buffer, 0, sizeof(buffer)); // 清零缓冲区
file.getline(buffer, 1024); // 读取一行数据
if (buffer[0] == '\0') // 读取的首字符 为 空
empty_line++;
else{ // 读取的首字符 非 空
int empty = 0; // 记录 空格 或 水平制表符
int note = 0; // 记录 注释行
int code = 0; // 记录 代码行
int i = 0;
for (; buffer[i] != '\0'; i++){
if (buffer[i] == ' ' || buffer[i] == '\t') // 出现 空格 水平制表符 记录
empty++;
else if (buffer[i] == '/') // 先出现 //
note++;
else if (buffer[i] != '/' && buffer[i] != '{' && buffer[i] != '}') // 先出现 其他字符
code++;
if (note > code){
note_line++;
i = -1;
break;
}
if (code > note){
code_line++;
i = -1;
break;
}
}
if (empty == i || empty == i - 1)
empty_line++;
}
}
cout << filename << " 文件的 代码行为 " << code_line << endl;
cout << filename << " 文件的 注释行为 " << note_line << endl;
cout << filename << " 文件的 空行为 " << empty_line << endl;
*c = code_line;
*n = note_line;
*e = empty_line;
}
⑥函数void recursion_function(char *parameter, char *filename)
:这个函数用来实现递归操作,最开始对递归操作的思考是件很让人头痛的事,再不同的学习新知识中,终于慢慢把这个问题解决掉了。由于加入了错误文件名判断,因此报错能力增强。
void recursion_function(char *parameter, char *filename){
intptr_t file;
_finddata_t fileData;
file = _findfirst(filename, &fileData); // 查找与输入匹配的第一个文件
if (file == -1)
{
cout << "当前目录下无与" << filename << "匹配的文件" << endl;
return;
}
do
{
char *_filename = fileData.name; // 获取文件名称
int count_char = 0; // 字符数统计
int count_line = 0; // 行数统计
int count_word = 0; // 单词数统计
int ount_code_line = 0;
int count_note_line = 0;
int count_empty_line = 0;
if (!strcmp(parameter, "-c")){
count_char = count_of_character(_filename); // 执行 字符统计操作
cout << _filename << " 文件的 字符数为 " << count_char << endl;
}
else if (!strcmp(parameter, "-l")){
count_line = count_of_line(_filename); // 执行 行数统计操作
cout << _filename << " 文件的 行数为 " << count_line << endl;
}
else if (!strcmp(parameter, "-w")){
count_word = count_of_word(_filename); // 执行 单词统计操作
cout << _filename << " 文件的 单词数为 " << count_word << endl;
}
else if (!strcmp(parameter, "-a")){
count_of_more(_filename, &count_code_line, &count_note_line, &count_empty_line); // 执行 更多复杂操作
}
} while (!_findnext(file, &fileData)); // 查找目录中的下一个文件
}
⑦函数void file_dialog()
:这个函数用来实现界面操作。因为获取系统文件目录并在界面显示,然后进行操作这样的功能是第一次接触,在前辈的指导下慢慢把这个东西做了出来,具体的语句功能都及时写在了注释里,避免遗忘。
void file_dialog(){
int count_char = 0; // 字符数统计
int count_line = 0; // 行数统计
int count_word = 0; // 单词数统计
int count_code_line = 0;
int count_note_line = 0;
int count_empty_line = 0;
wchar_t buffer[1024];
_tsetlocale(LC_CTYPE, TEXT("")); //让wprintf 支持中文
TCHAR szPathName[MAX_PATH*MAX_FILE_FOR_SEL];
OPENFILENAME ofn = { OPENFILENAME_SIZE_VERSION_400 };
ofn.hwndOwner = GetForegroundWindow(); // 打开OR保存文件对话框的父窗口
ofn.lpstrFilter = TEXT("文本文件(*.txt)/0*.txt/0C/C++源文件(*.cpp;*.c;*.h)/0*.cpp;*.c;*.h/0All Files(*.*)/0*.*/0/0");
lstrcpy(szPathName, TEXT(""));
ofn.lpstrFile = szPathName;
ofn.nMaxFile = sizeof(szPathName); //存放用户选择文件的 路径及文件名 缓冲区
ofn.lpstrTitle = TEXT("选择文件"); //选择文件对话框标题
TCHAR szCurDir[MAX_PATH];
GetCurrentDirectory(sizeof(szCurDir), szCurDir);
ofn.lpstrInitialDir = szCurDir; //设置对话框显示的初始目录
ofn.Flags = OFN_EXPLORER | OFN_ALLOWMULTISELECT | OFN_FILEMUSTEXIST; //如果需要选择多个文件 则必须带有 OFN_ALLOWMULTISELECT标志
_tprintf(TEXT("select file/n"));
there:
BOOL bOk = GetOpenFileName(&ofn); //调用对话框打开文件
if (bOk)
{
count_char = count_of_character(wchar2char(szPathName)); // 统计字符个数
count_line = count_of_line(wchar2char(szPathName)); // 统计行数
count_word = count_of_word(wchar2char(szPathName)); // 统计单词个数
count_of_more(wchar2char(szPathName), &count_code_line, &count_note_line, &count_empty_line); // 统计更多数据
swprintf_s(buffer, 1024, L"文件的 字符数 为 %d\n文件的 行数 为 %d\n文件的 单词数 为 %d\n文件的 代码行 为 %d\n文件的 注释行 为 %d\n文件的 空白行 为 %d"
, count_char, count_line, count_word, count_code_line, count_note_line, count_empty_line);
MessageBox(NULL, buffer, L"统计", MB_OK);
if (IDYES == MessageBox(NULL, L"是否继续查看其他文件信息?", L"统计", MB_YESNO)){
goto there;
}
}
}
7.实现结果及示例
①:基础功能
②:扩展功能1——行数详细信息
③:扩展功能2——递归统计
④:高级功能——图形界面
⑤:高级功能——文件统计
⑥:高级功能——文件统计结果
⑦:高级功能——界面递归确认
⑧:高级功能——递归显示文件信息
8.收获感悟
这次的项目很奇特,在最开始的时候,观察项目难度好像不大,结果越做越难,最终拖到压线惊险完成,这里很感谢队友SEM同学,一直耐心整理相关信息,不断及时提醒我各种节点的ddl,我们才能最终完成任务,也感谢前辈!!前辈的很多指点,让我少走了很多弯路。
经过了这次此项目,我有了以下这样的收获:
- 需求分析一定要从最开始就做好,匹配自己的能力及时获取新知识,才能跟上自己的时间安排,不至于手忙脚乱。
- 磨刀不误砍柴工!!对一种语言的探索是永无止境的,一个项目是一次挑战,这次挑战让我对C++有了更深的理解,包括学到了文件流数据流操作,系统路径获取等等,这对我以后的编程一定会大有帮助。
- 在新知识的学习方面希望自己可以更深入,这次项目因为时间问题,只是浅显的知道了怎么用,会有什么效果,却不知其所以然,因此不好保证在不看资料的情况下,还能完成的这么顺利。
- 结对项目和个人项目真的很不一样,沟通交流很重要,后期因为自己的时间很紧,队友也是一直监督着让我不能拉下工作,我觉得我们能完成这次项目,真的很棒。
以上就是我的收获和感想啦,居然有点开始喜欢上做软工项目了,真奇怪。
那就先给大家拜个早年吧,祝大家新春愉快,软工全过!!