【结对项目】单词计数

本文记录了一次结对项目——单词计数的实现过程,包括统计文本文件的字符数、单词数和行数,递归处理目录,以及实现GUI界面。项目中遇到了文件权限、参数检测、进程重定向等问题,并通过查阅资料和团队协作逐一解决。代码分析显示主要时间消耗在文件递归处理函数上。项目提升了团队合作和问题解决能力。

github链接 :https://github.com/BitWCG/WordCount

一、项目要求

1. 统计文本文件的字符数、单词数和行数

2. 递归处理目录下符合条件的文件

3. 返回更复杂的数据(代码行/空行/注释行)

4. 实现GUI界面

二、项目计划PSP表格

PSP2.1Personal Software Process Stages预估耗时    (分钟)实际耗时    (分钟)
Planning计划  
Estimate估计这个任务需要多少时间30 
Development开发  
Analysis需求分析(包括学习新技术)200 
Design Spec生成设计文档100 
Design Review设计复审(和同事审核设计文档)100 
Coding Standard代码规范(为目前的开发制定合适的规范)30 
Design具体设计100 
Coding具体编码1000 
Code Review代码复审100 
Test测试(自我测试、修改代码、提交修改)100 
Reporting报告  
Test Report测试报告150 
Size Measurement计算工作量30 
Postmortem & Process Improvement Plan事后总结,并提出过程改进计划60 
 合计2000 

三、解题思路

首先对主函数传来的参数进行检测,根据不同的参数执行不同的函数,从而得出相应的结果,同时,本项目对错误的检查能力很强,要求对不同的输入错误有不同的返回信息,具体参数及功能列表如下

参数功能
-c返回文件file.c的字符数
-w返回文件file.c的词的数目
-l返回文件file.c的行数
-s递归处理目录下符合条件的文件
-a返回更复杂的数据
-x打开GUI界面

项目需要编写的功能函数有关算法都比较简单,例如统计文件中字符数,读到正确字符,累加器+1;统计文件中词的数目,逐个字符进行读取,字母被分隔,相应的累加器就+1;统计行数时直接读取'\n'的数目即可。

功能的代码部分和其余功能将在后文进行说明,接下来,主要针对参数读取部分做详细解释。

 

根据输入的参数检查文件是否可以存在并可以读取,返回-1表示文件不存在,返回-2表示文件存在但是没有读取权限,返回1表示文件可以正常读取。

//检查文件是否存在且可以读取
int check_file_name(char file_name[])
{
	if (access(file_name, 0))//判断该文件/文件夹是否存在;
		return -1;
	if (access(file_name, 2) != 0)//判断该文件/文件夹是否有读权限;
		return -2;
	return 1;
}

在确定文件可以读取之后,还需要对输入参数的前半部分做一检测,判断其是否符合题目要求,返回为1则表明符合要求,返回为-1表明输入参数错误

//检查命令是否正确,命令正确返回1,错误返回-1.
int check_command(char command[])
{
	int len = strlen(command);
	if (len != 2 || command[0] != '-')
		return -1;
	if (command[1] == 'c' || command[1] == 'w' || command[1] == 'l' || command[1] == 'a') 
		return 1;
	else 
		return -1;
}

编写过程中的问题:

1. 在初次编写时,未考虑到检测参数长度的情况,故未调用strlen()函数,导致之后的测试中出现了问题,浪费了部分时间

2. 第一次编写时,没有考虑到文件权限问题,在测试文件过程中恰好测试了一个需要读取权限的文件,结果导致程序出现错误,在之后的debug过程中耗费了大量的时间

 

四、设计实现

总体设计(程序流程图)

 

五、各功能模块

1.  统计文件字符数 count_char()函数

//统计文件的字符数
int count_char(char file[])
{
	int char_num = 0;
	char ch;
	freopen(file, "r", stdin);//重定向输入
	while ((ch = getchar()) != EOF)
	{
		//if (ch != ' '&&ch != '\n'&&ch != '\t')
		char_num++;
	}
	fclose(stdin);
	return char_num;
}

 遇到的问题:

在这里有一个问题就是我们起初不知道如何进行进程的重定向输入输出,在经过查找资料后,才得以实现这个功能,以下是查找的博客链接:https://www.cnblogs.com/hsiang/p/6596276.html

2.统计文件单词数 count_word()

//统计文件的单词数
int count_word(char file[])
{
	int word_num = 0, flag = 0;
	char ch;
	freopen(file, "r", stdin);
	while ((ch = getchar()) != EOF)
	{
		if ((ch >= 'a'&&ch <= 'z') || (ch >= 'A'&&ch <= 'Z') || ch == '_') flag = 1;
		else
		{
			if (flag)
			{
				word_num++;
				flag = 0;
			}
		}
	}
	fclose(stdin);
	return word_num;
}

在编写该函数时遇到的问题:

在第一次编写的时候未考虑到字符为“_”的情况,所以在最后测试的过程中,得出的结果与预期不符

还有一点就是没有考虑到关闭文件,还是后来在审查的过程中发现这个小bug的

3. 统计文件的行数 count_line()

//统计文件的行数
int count_line(char file[])
{
	int line_num = 0;
	char ch;
	freopen(file, "r", stdin);
	while ((ch = getchar()) != EOF)
	{
		if (ch == '\n')
		{
			line_num++;
		}
	}
	fclose(stdin);
	return line_num;
}

 4. 统计文件的空行数count_blank_line()

//统计文件的空行数
int count_blank_line(char file[])
{
	int blank_line_num = 0, ch_num = 0;
	char ch;
	freopen(file, "r", stdin);
	while ((ch = getchar()) != EOF)
	{
		if (ch == '\n')
		{
			if (ch_num <= 1)
			{
				blank_line_num++;	
			}
			ch_num = 0;
		}
		else if (ch != ' '&&ch != '\t')
			ch_num++;
	}
	fclose(stdin);
	return blank_line_num;
}

遇到的问题:

在这里遇到一个小问题,就是我们在考虑编写这段函数的时候,一直在纠结是否直接检查“\n”,在这里是否可以检测到“\n”的存在,如果可以检测到的话,那之前的统计字符数的函数是否也将“\n”当作两个字符计算了进去,在经过小组讨论、查询资料和测试之后我们发现两者并不会冲突。

5. 统计文件的注释的行数count_comment_line()

该部分算法比较简单,只是在编写程序时需要注意一些细节方法,可以使程序显得简单明了

例如本小组用flag1是注释开头的标志位,current_line记录当前行是单行注释,flag2是多行注释开头的标志,line记录多行注释的行数,flag3是多行注释结尾的标志

//统计文件的注释的行数
int count_comment_line(char file[])
{
	//flag1是注释开头的标志位,current_line记录当前行是单行注释,flag2是多行注释开头的标志,line记录多行注释的行数,flag3是多行注释结尾的标志
	int comment_line_num = 0, ch_num=0, flag1 = 0, flag2 = 0, flag3 = 0, line = 0, current_line = 0;
	char ch;
	freopen(file, "r", stdin);
	while ((ch = getchar()) != EOF)
	{
		if (ch == '\n')
		{
			if (line&&ch_num > 0)//若line>0且字符数大于0表示当前行是多行注释中的一行
			{
				line++;
			}
			ch_num = flag1 = current_line = 0;
		}
		if (current_line == 1) continue;//当前行是单行注释,直接跳过对字符的判断直到读到换行符
		if (ch != ' '&&ch != '\n'&&ch != '\t') 
			ch_num++;
		if (flag2)//当前读到多行注释
		{
			if (ch != ' '&&ch != '\n'&&ch != '\t')
				ch_num++;
			if (ch == '*')//读到多行注释的结尾'*/'的符号'*'
				flag3 = 1;
			else if (ch == '/'&&flag3)//读到多行注释结尾的'/',必须连续读'*'和'/',否则flag3置0
			{
				comment_line_num += line;
				line = 0;
				flag2 = flag3 = 0;
				current_line = 1;
			}
			else
				flag3 = 0;
		}
		else if (ch == '/')
		{
			if (flag1 == 0)
				flag1 = 1;
			else if (flag1 = 1 && ch_num <= 3)//单行注释必须连续读取两个'/',且当前行在注释之前最多只有一个可显示字符
			{
				comment_line_num++;
				current_line = 1;
			}
		}
		else if (ch == '*')//多行注释的开头'/*'
		{
			if (flag1 == 1)
			{
				flag2 = 1;
				line = 1;//进入多行注释
			}
		}
		else
			flag1 = 0;
	}
	fclose(stdin);
	return comment_line_num;
}

6. wchar_t转换为string

编写该部分函数主要是为了使之后的输出和数据之间的传递更为方便

//wchar_t 转换 string 
void Wchar_tToString(std::string& szDst, wchar_t *wchar)
{
	wchar_t * wText = wchar;
	DWORD dwNum = WideCharToMultiByte(CP_OEMCP, NULL, wText, -1, NULL, 0, NULL, FALSE);// WideCharToMultiByte的运用
	char *psText;// psText为char*的临时数组,作为赋值给std::string的中间变量
	psText = new char[dwNum];
	WideCharToMultiByte(CP_OEMCP, NULL, wText, -1, psText, dwNum, NULL, FALSE);// WideCharToMultiByte的再次运用
	szDst = psText;// std::string赋值
	delete[]psText;// psText的清除
}

该部分代码参考博客:https://blog.youkuaiyun.com/chinahaerbin/article/details/7764483

7. 递归处理文件模块 search_file()

//递归处理文件模块
void search_file(string path, int idx)
{
	struct _finddata_t filefind;
	string cur = path + "*.*";
	int done = 0, handle;
	if ((handle = _findfirst(cur.c_str(), &filefind)) != -1)
	{
		while (!(done = _findnext(handle, &filefind)))
		{
			if (strcmp(filefind.name, "..") == 0)
				continue;
			if ((_A_SUBDIR == filefind.attrib)) 
			{  //是目录
				cur = path + filefind.name + '\\';
				search_file(cur, idx);    //递归处理
			}
			else //不是目录,是文件
			{
				int len = strlen(filefind.name);
				for (int i = 0; i < len; i++) 
				{
					if (filefind.name[i] == '.') 
					{
						len = i;
						break;
					}
				}
				if (strcmp(filefind.name + len, para[idx] + 1) == 0) 
				{
					cur = path + filefind.name;
					printf("%s:\n", filefind.name);
					for (int i = 1; i < idx; i++)
						basic_command(para[i], &cur[0]);
				}
			}
		}
		_findclose(handle);
	}
}

遇到的问题:

编写该部分函数遇到的问题主要是对c++语言了解的不够,所以在函数调用时出现了很大的问题,例如最初的不知道怎么处理文件名,之后不知道如何递归查找文件,还是在查找了资料才解决这个问题,参考的资料:https://fushengfei.iteye.com/blog/1100241

8.GUI界面

核心代码如下:

private void button1_Click(object sender, EventArgs e)
        {
            OpenFileDialog openFileDialog1 = new OpenFileDialog();   //显示选择文件对话框 
            openFileDialog1.InitialDirectory = "c:\\";
            openFileDialog1.Filter = "All files (*.*)|*.*";
            openFileDialog1.FilterIndex = 2;
            openFileDialog1.RestoreDirectory = true;

            if (openFileDialog1.ShowDialog() == DialogResult.OK)
            {
                this.FilePath.Text = openFileDialog1.FileName;     //显示文件路径 
            }
        }

        private void button2_Click(object sender, EventArgs e)
        {
            if (string.IsNullOrEmpty(this.FilePath.Text.Trim()))
            {
                MessageBox.Show("请选择文件");
            }
            string[] arg = new string[10];
            arg[0] = "-c"; arg[1] = "-w"; arg[2] = "-l"; arg[3] = "-a";
            arg[4] = this.FilePath.Text.Trim();
            StartProcess(@"WordCount.exe", arg);
        }

        private void Form1_Load(object sender, EventArgs e)
        {

        }
    }

遇到的问题:

在这部分遇到的最大的问题就是不会用VS编写GUI界面,所以从头到尾我们小组长认真学习了如何用VS写GUI界面,经过一天一夜的学习,终于写出了可以使用的GUI界面,从而使我们的项目更加完善。

9. 主函数

int main(int argc, char* argv[])
{
	if (strcmp(argv[1], "-s") == 0) // 递归处理目录下符合条件的文件  
	{		
		TCHAR szPath[MAX_PATH] = { 0 };
		if (GetModuleFileName(NULL, szPath, MAX_PATH))//通过函数GetModuleFileName获取执行程序的绝对路径。
		{
			(_tcsrchr(szPath, _T('\\')))[1] = 0;//这里需要用到_tcsrchr函数来将获取到的执行程序的绝对路径中的执行程序的名称去掉
		}
		string Path;
		Wchar_tToString(Path, szPath);
		for (int i = 0; i < argc; i++)
			strcpy(para[i], argv[i]);
		search_file(Path, argc - 1);
	}
	else if (strcmp(argv[1], "-x") == 0) //图形界面 
	{	
		ShellExecuteA(NULL, "open", "GUI.exe", NULL, NULL, SW_SHOW);
	}
	else //对单个.c文件的统计 
	{									
		int num_para;
		for (num_para = argc - 1; num_para > 1; num_para--) 
		{
			if (strcmp(argv[num_para], "-s") == 0 || strcmp(argv[num_para], "-x") == 0
				|| strcmp(argv[num_para], "-c") == 0 || strcmp(argv[num_para], "-w") == 0
				|| strcmp(argv[num_para], "-l") == 0 || strcmp(argv[num_para], "-a") == 0)
			{
				break;
			}
		}
		char  filename[500] = { 0 };
		strcat(filename, argv[num_para + 1]);
		for (int i = num_para + 2; i < argc; i++) 
		{
			strcat(filename, " ");
			strcat(filename, argv[i]);
		}
		int flag_3 = check_file_name(filename);
		if (flag_3 == -1) 
		{
			printf("Error:The file does not exist !\n");
			return 0;
		}
		else if (flag_3 == -2) 
		{
			printf("Error:The file has no read permissions !\n");
			return 0;
		}
		printf("%s:\n", filename);
		for (int i = 1; i <= num_para; i++) 
		{
			if (check_command(argv[i]) == 1)
				basic_command(argv[i], filename);
			else
				printf("Command parameter error !\n");
		}
	}
	return 0;
}

六、代码分析

 由以上可以看出,代码主要时间耗费在search_file()函数上,进入函数内部后,可以发现耗费时间的主要是由外部引入的代码,针对这部分代码我们并没有做过多优化,仅仅是修改了一下代码的内部结构

七、PSP表格总结

PSP2.1Personal Software Process Stages预估耗时    (分钟)实际耗时    (分钟)
Planning计划  
Estimate估计这个任务需要多少时间3050
Development开发  
Analysis需求分析(包括学习新技术)200200
Design Spec生成设计文档100120
Design Review设计复审(和同事审核设计文档)10080
Coding Standard代码规范(为目前的开发制定合适的规范)3030
Design具体设计100150
Coding具体编码10001500
Code Review代码复审100200
Test测试(自我测试、修改代码、提交修改)100100
Reporting报告  
Test Report测试报告150150
Size Measurement计算工作量3050
Postmortem & Process Improvement Plan事后总结,并提出过程改进计划6060
 合计20002690

 

八、总结与反思

       这一次是做结对项目,俩人合作完成单词计数。难点主要在某几个功能函数的实现和GUI界面的设计上。

       代码编写是两个人编写,所以需要先约定代码风格,比如采用下划线命名法,分文件写的时候,先规划每个函数实现功能,精确到参数个数及类型,返回值类型,然后分开写的时候就算不知道对方写的部分也能调用函数。结对项目与个人项目不同,它更加考察我们对项目的分工与合作,如何把一个大的项目拆分成多个小项目,如何多人合作,程序连接时如何能正确执行,各个模块间接口如何,这些是本次项目所带给我们最多值得学习的地方。还有就是我在本项目中感到困难的地方就是,出现问题需要协商,协商的过程中不愿意放弃自己的想法,就很容易造成困扰。解决这个问题还需要改变自己的观念,如果队友的解决方案的确良好可行,应该去聆听并且共同对方案进行实现。

       总之,这次的项目带给我很多经验与收获,也让我学会了如何与他人合作开发项目,今后我一定好好学习,争取将这份宝贵的经验融入到我日后的学习工作中。
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值