github地址:https://github.com/star-mick/wcproject
PSP
阶段: | 估计耗时(min) | 实际耗时(min) |
计划: | 5 | 3 |
计划所需时间 | 5 | 3 |
开发: | 105 | 22.5*60 |
需求分析(包括学习新技术) | 20 | 25 |
生成设计文档 | 5 | 2 |
设计复审 | 5 | 2 |
代码规范 | 5 | 2 |
具体设计 | 10 | 30 |
具体编码 | 30 | 17*60 |
代码复审 | 10 | 2*60 |
测试 | 30 | 2.5*60 |
报告: | 35 | 2.25*60 |
测试报告 | 20 | 2*60 |
计算工作量 | 5 | 5 |
事后总结,并提出过程改进计划 | 10 | 10 |
合计: | 140 | 24*60 |
对题目的思考:
读完题目后,感觉需求非常的明确,只涉及对文件的读写操作,基本功能相对简单,可以在了解需求的时候就想清楚代码的设计,唯一的设计难点是-a的功能,因为状态较多,所以 需要额外的分析设计。
统计字符数,单词数和行数,是在对文件的的一次遍历时对结果的简单判别,并不值得讨论。禁用单词的功能使在单词统计的基础上读出单词并与禁用词表进行比较,如果采用C++,或是java 有对字符串的一系列函数可以直接调用,我是用C操作的,C 有一个简单的函数 char* strstr(char*,char*);可以查看第一个字符串是否包含第二个字符串,可以简化-e的功能,但是由于将要读取的字符串的长度未知,此时需要使用链表将所读取到的字符拼接成字符串的形式,这涉及链表的插入删除和创建。
还有两个不太熟悉的功能,一是向main函数传参并读取,经过查阅百度很快就解决了,网上有很多解决方法。二是对文件夹的遍历并返回文件的名称,这里需要用到<io.h>这个头文件,其中的函数_finddata(),和函数findnext()可以解决对文件夹的相关操作,需要了解可以在网上直接百度得到。
-a的功能需要建立每一行的状态图,因为状态相对较多,所以非常复杂,但却没有太多思考。
最后需要对代码进行测试,进行单元测试,就是要对每一个代码模块进行正确性测试,因为该项目中的每个代码模块通常就是一个功能,所以测试和校验相对明确,即对每个功能模块进行测试。
程序实现过程:
本程序的功能相对独立,所以在设计时所包含每个函数都独立的完成项目的一项功能,main 函数对输入参数进行处理,解析出需要调用的模块和相关的文件路径,包括对文件夹的分析,然后由5个独立的函数分别实现字符统计、单词统计、行数统计、禁用词表后的单词统计、单词行注释行空行的统计。main 函数根据需要调用相关的函数进行统计输出。
但这些函数相对简单,只有最后的-a功能需要设计复杂的流程处理,我在设计时考虑到了所有可能出现的情况,共设计了9中状态,由于过于复杂,就不画流程图了,之后我参看了其他同学的状态分析,他门的处理相对简单,更便于分析和设计,因此在对实际情况进行分析设计时应该简化状态来高效的处理关键的状态迁移,而后才是由简入繁式的更新代码,这是我所学到的。
代码说明:
这是所涉及的函数,每个函数完成一项功能,其下是建立字符节点的结构体和单词的的结构体,单词的结构体包含字符链表,存储一个字符串。
//分别计算字符数,单词数,行数,(代码行,空行,注释行),带禁用的单词统计
int countcn(FILE*);
int countwn(FILE*);
int countln(FILE*);
int* countal(FILE*);
int countwn2(FILE* f, FILE* f2);
struct charlink
{
char c;
charlink* next;
};
struct word
{
charlink* mychar;
int len;
};
下面是-a功能的实现代码,基本每一句代码都有注释。
//统计单词行,空行,注释行
int* countal(FILE* f)
{
int x[3]={0};
int ln=0;//记录行状态
char ch,ch1='a';
int lm = 0;//记录注释类
int keword = 0;
while ((ch = fgetc(f)) != EOF) keword++;
fseek(f,0L,SEEK_SET);
while (keword--)
{
if (keword>0)
ch = fgetc(f);
else
ch = '\n';
//printf("%d ", ln);//测试状态迁移
lm = 0;
//进注释
if (ch == '/'){
ch1= fgetc(f);
if (ch1 == '*')lm = 1;//出现/*
else if (ch1 == '\n') ch=ch1;// /后回车(假定不会出现)
else if (ch1 == '/')lm = 3;//出现//
else if (ch1!=' '&&ch1!='\t')
ch1 = 'x';//双字符
}
//出注释
else if (ch == '*'){
ch1 = fgetc(f);
if (ch1 == '\n');//*后回车
else if (ch1 == '/') lm = 2;//出现*/
else if (ch1!= ' '&&ch1!= '\t')
ch1 = 'x';//双字符
}
if ((ch == '\n'&&ln == 1 && ch1 != '\n') || (ch == '\n'&&ln == 0))//空状态本行有一个或0个字符
x[1]++, ln = 0, ch1 = 'a';
else if (ch1 == '\n'&&ln == 1)//空状态本行有2个字符
x[0]++, ln = 0, ch1 = 'a';
else if (ch == '\n' && ln == 5)//空注释
x[1]++;
else if (ch == '\n'&&(ln == 6||ln==8)) x[0]++, ln = 0;//表示本行有代码x
else if (ch == '\n'&&ln == 2) x[0]++, ln = 5;//进入x/*后的回车
else if (ch == '\n'&&ln == 7) x[2]++, ln = 5;//进入/*后的回车
else if (ch == '\n' && ln == 9) x[2]++,ln = 0;//非空注释状态//下换行
else if (ch == '\n' && ln == 3) x[2]++,ln = 0;//非空注释状态*/下换行
else if (lm == 1 &&ln == 6) ln = 2;//代码后跟进注释/*
else if (lm == 1 && ln == 3) ln = 7;//出注释后有/*
else if (lm == 3 &&ln == 6) ln = 8;//代码后跟全注释x//
else if (lm == 3 && ln == 3) ln = 9;//出注释后有//
else if (lm == 2 &&(ln == 5||ln==7)) ln = 3;//注释/*,后出注释
else if (lm == 2 &&ln == 2) ln = 6;//代码后进注释后出注释
else if (lm == 1 && ln == 0) ln = 7;//直接进入注释状态/*
else if (lm == 3 && ln == 0) ln = 9;//直接进入完全注释状态//
else if (ch1 == 'x'&&ln == 0) ln = 6, ch1 = 'a';//空状态下两个字符
else if (ch != ' '&&ch != '\t' &&ln == 3) ln = 6;//出注释后有代码
else if (ch != ' '&&ch != '\t'&&ln == 5) ln = 7;//空注释状态下有字符
else if (ch != ' '&&ch != '\t'&&ln == 1) ln = 6;//进入代码状态
else if (ch != ' '&&ch != '\t'&&ln == 0) ln = 1;
}
return x;
}
这段代码就是对某行状态的有限状态迁移的分析,ln记录本行状态,共8种状态,lm记录所遇到的特殊字符,即出现的注释字符,ln 0-9分别表示(本行为空且不再注释之内,本行有一个代码,本行前面是代码后面是/*~,本行前面是注释后面是*/,无意义,本行是空但在注释之内,本行只有代码,本行只有注释且在/*~*/之内,无意义,本行开头是//~)。代码针对读取到的不同的字符将状态进行转换。(4,8无意义)
以下是与禁用词表相关的内容:读取文件中的字符并建立单词的链表:myword是单词结构体,每读取一个字符,就向单词中的字符链表插入字符,需要在最后插入\0;
int i, j, kword;
word* myword;
myword = (word*)malloc(sizeof(word));
。。。。
//以下是关键部分
if (kword!=0&&(ch >= 97 && ch <= 122) || (ch >= 65 && ch <= 90))
{
if (state == 0)//开始记录单词
{
ones = (charlink*)malloc(sizeof(charlink));
ones->c = ch;
ones->next =NULL;
myword->len = 1;
myword->mychar = ones;
p1 = ones;
state = 1;
}
else //正在记录单词
{
ones = (charlink*)malloc(sizeof(charlink));
ones->c = ch;
ones->next = NULL;
myword->len++;
p1->next = ones;
p1 = ones;
}
}
else if (state == 1||kword==0)
{
state = 0;
ones = (charlink*)malloc(sizeof(charlink));
ones->c = '\0';
ones->next = NULL;
myword->len++;
p1->next = ones;
oneword = 1;
}
建立数组sign[]来记录输入的参数,以便于后续的判断
for(i=1;i<argc;i++)
{
if( argv[i][0]=='-')//判断输入参数
switch (argv[i][1])
{
case 'c':sign[0]=1;break;
case 'w':sign[1]=1;break;
case 'l':sign[2]=1;break;
case 'o':sign[3]=1;break;
case 'a':sign[4]=1;break;
case 'e':sign[5]=1;break;
case 's':sign[6]=1;break;
default:
printf("输入参数有误"); break;
}
else if(argv[i-1][1]=='e')
stoplist=argv[i];
else if (argv[i - 1][1] == 'o')
result = argv[i];
else source=argv[i];
}
dir = source;
测试设计过程:
由于该项目的功能相对独立,采用单元测试,针对每个参数进行一次测试,就可以测试各个函数是否正确,针对含有判定的函数,可以设计测试文件的内容,使得每种情况都会被遇到,达到完全覆盖。测试代码采用的是bat的start函数多次调用wc.exe文件并采用不同的参数和输出文件,编写完成后,直接运行bat就可以在文件夹中查看输出文件的内容并进行校验。