代码编写过程中的重要知识点
VS调试命令行参数的输入
在VS中调试,无法直接输入命令行参数,但是可以通过一下方法配置命令行参数:
- 点击菜单栏的 项目>>属性
- 出现属性对话框之后,选择 配置属性>>调试>>命令参数:
- 在里面设置main的参数即可,多个参数用空格隔开。
String相关
erase操作
在对字符串的处理过程中需要除去字符串最后的数字,这里利用了erase来进行操作:
erase操作有三种:
- 指定pos和len,其中pos为为起始位置,pos以及后面len-1个字符串都删除
- 迭代器,删除迭代器指向的字符
- 迭代器范围,删除这一范围的字符串,范围左闭右开
//代码来自cpp官网 int main () { std::string str ("This is an example sentence."); std::cout << str << ‘\\n‘; // "This is an example sentence." str.erase (10,8); // ^^^^^^^^ //直接指定删除的字符串位置第十个后面的8个字符 std::cout << str << ‘\\n‘; // "This is an sentence." str.erase (str.begin()+9);// ^ //删除迭代器指向的字符 std::cout << str << ‘\\n‘; // "This is a sentence." // ^^^^^ str.erase (str.begin()+5, str.end()-9); //删除迭代器范围的字符 std::cout << str << ‘\\n‘; // "This sentence." return 0; }
在我的代码中,在 Handlestring() 函数中,通过从后往前遍历得到最后一个字母的位置i,利用第三种方法完成删除操作:
s.erase(s.begin() + i + 1, s.end());
怎么将一个 char 添加到 std::string 后面?
直接用append方法的话需要先将char转换成string,比较麻烦;实际上可以通过直接使用 += 运算符来完成这个功能:
s += c;
大小写转换
C++中没有string直接转换大小写的函数,需要自己实现。一般来讲,可以用stl的algorithm实现:
#include <string> #include <algorithm> int main() { string s = "ddkfjsldjl"; transform(s.begin(), s.end(), s.begin(), tolower); // or transform(s.begin(), s.end(), s.begin(), toupper); if you want to transform to Upper cout<<s<<endl; return 0; }
但是在使用g++编译时会报错:
对‘transform(__gnu_cxx::__normal_iterator<char*, std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, __gnu_cxx::__normal_iterator<char*, std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, __gnu_cxx::__normal_iterator<char*, std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, <unresolved overloaded function type>)’ 的调用没有匹配的函数。
这里出现错误的原因是Linux将toupper实现为一个宏而不是函数,这里由两种解决方案:
-
transform(str.begin(), str.end(), str.begin(), (int (*)(int))toupper);
这里(int (*)(int))toupper是将toupper转换为一个返回值为int,参数只有一个int的函数指针。
- 自己实现ToUpper函数:
int ToUpper(int c) { return toupper(c); } transform(str.begin(), str.end(), str.begin(), ToUpper);
Map相关
unordered_map
最初我的代码使用的是普通的map,后来改成了unordered_map,主要有以下原因:
unordered_map和map类似,都是存储的key-value的值,可以通过key快速索引到value。
但不同的是unordered_map不会根据key的大小进行排序,存储时是根据key的hash值判断元素是否相同,即unordered_map内部元素是无序的,而map中的元素是按照二叉搜索树存储,进行中序遍历会得到有序遍历。
而对我们的任务来说,使用map的主要原因是方便根据key来找到相应的value,(这里的key就是标准化后的word,而value根据不同的map有差别,什么是标准化在前一篇博客中有写),对顺序没有要求,所以使用unordered_map可以免去排序的麻烦,实践证明这也大大提高了程序的运行速度。
查看当前是否包含key
这里我只想完成一个功能,就是:我得到一个特定的key,先查看在map中是否有该key的key-value对了,如果有我根据当前的value来做相应操作,如果没有则给他一个默认的value并加入map中。
map提供了两种方式,来完成这个功能,查看是否包含key,m.count(key),m.find(key)。
m.count(key):由于map不包含重复的key,因此m.count(key)取值为0,或者1,表示是否包含。
m.find(key):返回迭代器,判断是否存在。
这里使用了第一个,因为比起第二个要快一些
一个key对应多个value
其实我最初的设想就是利用一个map,来完成次数统计和表达式更新的,但是奈何默认的 map[key]++ 方法只对 map<string, int>型的map适用,所以不得已将一个map分为了两个,一个存储频率数,一个存储最小表达式,所幸对性能影响不大。
unordered_map<string, int> words_map; //用来记录word出现次数的hash表 unordered_map<string, int> phrase_map; //用来记录词组出现次数的hash表 unordered_map<string, string> exp_map; //用来记录单词对应的最小表达式的hash表
vector相关
查找一个vector中的最大值、最小值与index
利用max_element,min_element,distance
int main() { std::vector<double> v {1.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0, 3.0, 4.0, 5.0}; std::vector<double>::iterator biggest = std::max_element(std::begin(v), std::end(v)); std::cout << "Max element is " << *biggest<< " at position " << std::distance(std::begin(v), biggest) << std::endl; auto smallest = std::min_element(std::begin(v), std::end(v)); std::cout << "min element is " << *smallest<< " at position " << std::distance(std::begin(v), smallest) << std::endl; }
输出:
Max element is 5 at position 4 min element is 1 at position 0
最初想用它来算TOP10,但是后来想了想还是自己写了个算法
debug总结
这里大致总结了一些遇到的一些比较关键的bug及其排查解决过程
低级错误
在写代码调试的过程中犯了不少低级错误,还好最后都基本排查出来了,但是真的让人感到不快,例如:
在判断是否是字母这个逻辑中:
c >= ‘a‘ && c <= ‘z‘
竟然将 ‘z‘ 手误写成了 ‘b‘,导致了进一步的例如指针越界之类的错误,又因为这个错误很小导致排查了半天。
递归读取文件问题
之前写的递归读取文件夹下所有文件在测试集上运行良好,但是这些都是我以文件加路径作为输入完成的。在一次测试过程中我只输入了一个文件的路径,结果竟然不能识别,于是排查那个遍历文件的函数,发现当初写的时候没能考虑用户输入是单个文件的情况,于是及时对该函数做了更改。
void GetAllFiles(string path, vector<string>& files) { long hFile = 0; struct _finddata_t fileinfo; string p; files.push_back(path); //加上了这一句 hFile = _findfirst(p.assign(path).append("\\\\*").c_str(), &fileinfo); if (hFile != -1) { do { if ((fileinfo.attrib & _A_SUBDIR)) { if (strcmp(fileinfo.name, ".") != 0 && strcmp(fileinfo.name, "..") != 0) { files.push_back(p.assign(path).append("\\\\").append(fileinfo.name)); GetAllFiles(p.assign(path).append("\\\\").append(fileinfo.name), files); } } else { files.push_back(p.assign(path).append("\\\\").append(fileinfo.name)); } } while (_findnext(hFile, &fileinfo) == 0); _findclose(hFile); } }
移植问题
这个问题在我之前的一篇博客中做了详细介绍:
第一次个人作业【二】(代码跨平台的简单解决方法,附代码!!)
中文字符串乱码
早先在做测试时,并没有使用命令行传递参数,而是将路径直接写在了程序中,但是出现了一个问题:只要路径中含有中文字符,文件就无法读取。
最开始我以为是选用的库文件不支持中文字符集。网上搜索,将所有的办法试了一遍都不太行,当时差点崩溃。后来还是老老实实设置断点debug才发现写在程序中的中文路径编译后产生了乱码,自然就无法定位文件。之后顺藤摸瓜才发现是VS2015编辑器的锅,VS2015使用的编译器是Roslyn,而这个问题与Roslyn检测文件编码的处理方式有关。真是坑别的版本都没有的问题,偏偏被我遇到了。
发现问题后赶紧换成了用命令行传参,问题得到解决。
单词个数统计误差太大
之前统计的单词个数与助教答案相差有10万之多,怎么debug都无法找到原因,后来更改了字符串处理的逻辑,将误差减小到1万左右。
文件的二进制方式读取与文本方式读取
这里也是个天坑,最初我读取文件是用的二进制方式,但结果与助教结果差别太大,后来明白是由于在Windows下文件编码格式的问题,在Linux下就不存在了,例如 \\n -> \\r\\n 这个问题。
后来改成文本方式读取,算是成功解决。