PSP 表格
Personal Software Process Stages | 任务内容 | 预估耗时(分钟) | 实际耗时(分钟) |
---|
Planning | 计划 | 45 | 53 | Estimate(估计时间) | 估计这个任务需要多少时间,并规划大致工作步骤 | 45 | 53 | Development | 开发 | 1280 | 1584 | Analysis | 需求分析(包括学习新技术) | 180 | 300 | Design Spec | 生成设计文档 | 60 | 51 | Design Review | 设计复审 | 20 | 22 | Coding Standard | 代码规范 | 15 | 10 | Design | 具体设计 | 180 | 188 | Coding | 具体编码 | 800 | 960 | Code Review | 代码复审 | 240 | 300 | Test | 测试(自我测试,修改代码,提交修改) | 420 | 400 | Reporting | 报告 | 150 | 113 | Test Report | 测试报告 | 30 | 20 | Size Measurement | 计算工作量 | 30 | 23 | Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 90 | 70 | Total | 合计 | 1475 | 1750 |
解题过程
要求分析
编程任务分解
- 文件读入
- 关键字提取 switch - case结构
- if - else 结构
- if - else if - else结构
- 传入代码路径及完成程度的参数
作业要求
代码规范附于GitHub中
思路说明
- 初步思路
1、读入文件:运用相关函数,按行读取代码文件,并将其拼成字符串
2、判断关键字:运用 STL 容器 map 建立 string 到 int 的映射,当由文件转换成的字符串中有能和关键字匹配的字符串,则增加相应字符串对应的数值。
3、switch case结构:特判读入的字符串是不是switch 或者case, 为了记录不同switch对应的case的个数,建立一个存放整型的vector。
4、判断两种条件分支语句的个数:运用统计得到的if(包括else if中的if), else(包括else if中的else),“else if” 个数相互加加减减得到。算了半天发现这个思路是行不通的 orz ,详见下文。
5、用main函数的两个参数传入文件路径和完成程度。
- 更正与改进思路(9.16)
1、印在杯子上的人民币不是人民币(也不可以把人民币印在杯子上) 要对出现在字符串形中以及注释中的关键字进行特殊处理。 在读取的时候跳过双引号,注释覆盖的文本。
2、上述判断条件结构的方法有问题。首先这种方法把有立体结构的分支结构线性化了,即前后出现的单个 “else” 和单个 “if” 一定能构成一个 “else if” 。实则不然,此处还忽略了程序中花括号的存在。于是还是选择用栈的方式处理条件分支语句的判断。
ps:群里面聊着聊着,我就发现自己的脑回路又和大家伙的不一样了,功能实现和题意有出入,那就改吧。因为理解的问题,后来一段时间里面认为"if… elseif…" 这样的语句也属于if_elseif_else语句,于是还改出来一版满足这个要求的。上午改好,下午就恢复。(猝不及防用到了git restore)
- 再更正和改进思路(9.18)
1、不需要每一次都遍历map来计算每一个关键字的个数。换言之,对于在之后会特判的"if",“else”,“switch”,“case”,只需要用一个整型(假设整型足够)来记录即可。
参考资料
设计实现
代码组织方式
设计两个类,SwitchPart和IfPart。前者封装与统计 switch-case 结构有关的变量和函数;后者封装与统计 if-else,if-elseif-else 结构有关的变量和函数。
读入文件并将其处理为一个字符串。每次读取字符串中的一个不带空格的子字符串。并判断是否是关键字,若是则记录。最后输出相关的信息。
下图展示的是两个类中用于统计的函数的实现流程图
流程图
Switch部分处理流程图
If部分处理流程图
代码说明
字符串处理
- 按行读入文件,行与行之间要加入空格,以免前一行的最后一个字符串和后一行的最开始的字符串错误拼接在一起。由于只要判断关键字,故把读入的符号变换成空格,如此不论其与关键字之间是否有空格,都不会影响关键字的匹配。
string TransformFileToString(string file_position)
{
ifstream in_file;
in_file.open(file_position);
if (!in_file.is_open()) //打开文件失败
{
cout << "open file error" << endl;
return 0;
}
string string_in_file, temp;
while (getline(in_file, temp))
{
int i = 0;
while(temp[i] == ' ')
++i;
if(temp[i] == '/' && temp[i+1] == '/') //no need to transform "//" kind annotation
continue;
string_in_file += temp;
string_in_file.append(" "); //add space between adjacent lines
}
in_file.close();
return string_in_file;
}
- 处理初步得到的字符串中的花括号,"/**/"类型的注释,以及引号中的内容。
花括号被保留,并在其前后都加上空格。 "/**/"类型的注释及其中间的内容被删去。 引号及其中间的内容被删去。
string file_to_string;
for (int i = 0; i < string_in_file.length(); ++i)
{
if(string_in_file[i] == '\"' ) //no need to transform words in double quotation marks
{
++i;
while(string_in_file[i++] != '\"');
}
else if(string_in_file[i] == '/' && string_in_file[i+1] == '*' ) //don't transform "/**/" kind annotation
while(string_in_file[i] != '*' || string_in_file[i+1] != '/')
++i;
else if(string_in_file[i] == '{' || string_in_file[i] == '}') //retain braces
{
file_to_string += ' ';
file_to_string += string_in_file[i];
file_to_string += ' ';
}
else if (ispunct(string_in_file[i])) //transform punctuation into space
file_to_string += ' ';
else
file_to_string += string_in_file[i];
}
return file_to_string;
特殊结构处理
- 统计switch的方式比较直接:判断读入的字符串是不是"switch"或者"case";如果是,就统计个数,并且加入是否出现了新的switch的判断,以实现统计从属不同switch的case的个数的目标。
当没有再次读入一个switch时,就在最后加入计算case个数的vector的数字上进行增加;当再次读入一个switch时,在vector中新加入一个元素1。
int SwitchPart::Count(string temp)
{
if(temp == "case")
{
if(switch_num_change_ == 0)
{
int back = case_array_.back();
case_array_.pop_back();
case_array_.push_back(++back);
}
else
{
case_array_.push_back(1);
switch_num_change_ = 0;
}
case_num_++;
}
else if(temp == "switch") // a new switch appear
{
switch_num_change_ = 1;
switch_num_++;
}
else
return 0;
return 1;
}
- 统计两种条件分支结构的个数,思路和算法结构中“匹配括号” 的思路一致。运用栈来保存相关字符串,只有遇到else或者右括号才出栈。
正是为了实现上述功能,在处理读入文件为字符串的时候,就不能将大括号变换成空格了,要加入特判。
int IfPart::Count(string& temp,stringstream& ss)
{
if(temp == "if" || temp == "{")
{
if(temp == "if")
if_num_++;
if_else_stack_.push(temp);
}
else if(temp == "else") //else or elseif appear
{
else_num_++;
ss >> temp;
if(temp == "if") //elseif appear
{
if_num_++;
if_else_stack_.push("elseif");
}
else //else appear
{
int flag = 0;
while(if_else_stack_.top() != "if")
{
if(if_else_stack_.top() == "elseif")
flag = 1;
if_else_stack_.pop();
}
if_else_stack_.pop();
if(flag == 1) //elseif has appeared
if_elseif_else_num_++;
else
if_else_num_++;
if(temp == "{")
if_else_stack_.push("{");
else
return 0;
}
}
else if(temp == "}")
{
while(if_else_stack_.top() != "{")
if_else_stack_.pop();
if_else_stack_.pop();
}
else
return 0;
return 1;
}
- 遍历map统计关键字个数,在加上特判的字符串的个数,即可完成要求。最后依照传入的参数实现不同等级的输出。
单元测试
单元测试运用的是Cutest,输出的界面可谓十分简洁。(一开始尝试的是 CppUnit,可是配置了好久都跑不起来,所以只好暂时转移阵地了)具体代码附在GitHub上。
实现方式是:将要测试的函数输入CuTest.c的文件中,在CuTestTest.c的文件中调用待测函数,并给出预测结果,将两者进行比对。如果有多组同一个函数的测试数据,可以将他们加入到同一个suite中,最后由AllTest.c执行整个suite中的测试。 结果截图:前一行输出的点表示通过的测试。假如没有通过测试则会输出“F”。 后一行输出的是对测试结果的描述,图示是都通过了测试的情况(11个测试全部通过)。 (举个栗子)假如没有通过测试 会显示哪个测试没有通过,实际值和理论值各自是什么。
优化
单元测试覆盖率优化
可以通过增加测试样本数量和单个测试样本大小,覆盖更多的分支。不仅仅用语句覆盖率来衡量覆盖情况,更是要注重路径的覆盖率。在分支语句中有两个条件的时候,也要将两个条件都纳入到测试的范围。
性能测试
本次使用的性能测试工具是Intel VTune Profiler。Windows和Linux下都能安装。但是虚拟机上有一些测试功能会受限。下图展示的是 VTune Performance Snapshot的结果,也可以通过更进一步分析热点函数,访存方面的性能。
Intel VTune Profiler性能报告
在内存绑定的指标中,展示了L1-L3级缓存上停止的频率,也可以看到主内存上停滞的还是比较高的。说明了由于负载或者存储而进行的等待的时间。 但是由于输入数据不够大的原因,测来的结果不够准确。
性能优化
由于代码的实现方式强烈依赖读入字符的顺序,所以在许多重要环节均无法使用并行处理。而遍历map的操作虽然理论上不存在数据依赖可以并行处理,但是map容器是不支持并行的。
优化的方向从并行处理调整为优化传参以及减少不必要的计算开始。
- 初代的代码中函数的参数中会有整张map表,后改为不再传入map表,仅仅统计相关关键字的个数,存放在整型(此处假设整型足够存储)中,只传递整型。
- 初始的代码对完成等级(1,2,3,4)的控制仅仅体现在最后打印的一步,前面的代码都能完成最高等级的要求。优化过后,增加一个在处理阶段就能按照完成等级调用相关函数,实现对不同等级的不同处理,减少了实现低完成等级时不必要的操作。
总结
困难
1、在完成题目的拔高要求和终极要求的时候,由于一开始陷入了用数学的方法加上通过特判以及变量之间的加加减减来完成的思路之中,折腾了有一阵。后来不是在纸上枚举可能出现的情况,而是直接上手写一小段分支的结构,突然就跳出了之前的思维定式。所以转换解题姿势能够带来新思路。
2、头一次真正用到git来上传文件,之前都是纸上谈兵。整个过程会有许多预见不了的小问题。像git commit -m" "附带的说明同时加到了两个文件之上,应该怎么更改之类的问题是没有实践难以发现的知识盲区。经过面向CSDN的一通补缺补漏,才最终了解了上传文件的套路。
有待提升
1、首先是对任务量和完成时间的估计。实际完成时间超过预估时间的模块挺多的,只能说在估计的时候过分乐观了,没有预料到返工,返工,返工会耗费这么多时间。
2、这次的代码迭代更新的过程有不太妥的地方。本来想先写一个函数实现基础的功能,最后居然就直接写出了实现四个完成等级的函数躺在代码里面。后来才考虑到需要写类来封装一下。这个也是前期设计的时候考虑不周的地方。
3、在代码的书写方面,之前也是特立独行 有对其他阅读代码的人或者未来的自己不太友好的风格 。这次是按照《Google 开源项目风格指南》和自己的风格定的规范,但是实际执行的时候时时刻刻都遵守仍然有难度,偶有一整份代码看下来发现写得太开心,函数太长了,或者一行的字符个数超出规范的情况。 4、用的是C++来编程,一定程度上是在自己的舒适圈中。之后也要将另外两种语言在合适的机会用起来。
做得不错
1、这次没有一点拖延症!!从拿到题目开始,就一直有按照分配来完成作业的时间进行。遇到问题就交流问题,解决问题。
2、发现自己有的时候还是很有能量的嘛。因为单元测试一开始编译不通过的原因,就在反复地调试+请教同学,历经了好多好多小时最后还是坚持下来,终于是能明白源码的功能并使用它了。虽然想过要不就自己写一个测试函数来测,下一次再学,但是又怕永远地鸽下求,就卯足劲一口气解决问题。
3、由于自己写过的代码行数和用到的关键字类型不够全面,就会想到去到网上找了和用自己用来训练的代码量比较大的文件来测试。
|