你值得拥有
在实际工作过程中,字符串分割绝对是一个必要技能,因为很多场合下都会用到这个,例如说经典csv文件读取,其中需要用“,”进行分割。另外“.”,“-”等等各种各样,都会出现,还要考虑出现多次等情况,一段小小的处理代码都可能包含着复杂无比的逻辑,Bug也随时可能出现。
既然来到了C++的世界,自然要简化方法,站在更高的层次来指挥代码,下面就来分析一下伟大航路的“新世界”,Are you ready?!
原始方法
在“新世界”之前,看一下之前C语言的时代,是如何进行字符串分割的呢?那自然就是遍历字符串,找到对应的“分隔符”,然后将分隔符之前的内容进行保存等等。
如果“分隔符”有多种,例如说空格,逗号等等;“分隔符”有多个等场景,要适配这么一个负责的算法,实在是很麻烦,并且要保证无Bug,那还要考虑各种字符串相关的问题。虽然程序员们都比较喜欢挑战,但是费时费力写这么个函数实在是效率太低。
相关例子我就不浪费智商了,主要还是因为我太懒,有兴趣的可以百度一下,相信会有很多代码。
新的方法
第一种方法:Boost库中的boost::split。
优点:快捷方便,不需要考虑各种情况,根据函数说明传入各种参数即可出结果。
缺点:需要加入Boost库。
接下来就是我喜欢的方法了,用“流”来分割字符串,一种高大尚的感觉油然而生,废话有点多,老样子,先来看一段代码:
int main(void)
{
std::string rawStr{"Welcome to the new world"};
std::stringstreamss{rawStr};
std::vector<std::string> strVec{std::istream_iterator<std::string>{ss},
std::istream_iterator<std::string>{}};
std::copy(strVec.begin(),strVec.end(),
std::ostream_iterator<std::string>{std::cout, "\n"});
return 0;
}
好嘞,简单几行,代码已按照空格分割完毕,相关代码只有两行,是不是很犀利呐?
继续深入
正如《如何阅读一本书》里面提到的一样,不要浅尝辄止,随时多问自己几个问题。如果现在不是安装空格进行字符串分割呢?上面的代码还是有效的吗?
答案自然是:No!那如何解决?
方案一,流读入时进行判断,其实本质上又回到了之前的问题,不推荐这种方式。还是把例子写出来,大家可以参考一下:
int main(void)
{
std::string rawStr{"Welcome,to,the,new,world"};
std::stringstreamss{rawStr};
std::vector<std::string> strVec;
std::string splitStr;
while (ss)
{
if (!std::getline(ss,splitStr, ',')) break;
strVec.push_back(splitStr);
}
std::copy(strVec.begin(),strVec.end(),
std::ostream_iterator<std::string>{std::cout, "\n"});
return 0;
}
缺点很明显,如果分隔符有多种时,逻辑又会复杂的让人崩溃。
方案二,重设locale,locale中设置分隔符是哪些,再一次,高大尚的气息迎面扑来。知识获取的快感压抑不住,废话总是有点多,代码如下:
class StringSplitLocal : public std::ctype<char>
{
public:
StringSplitLocal() :std::ctype<char>(GetSplitTable()) {}
staticstd::ctype_base::mask const *GetSplitTable()
{
staticstd::ctype_base::mask *masks = nullptr;
if (nullptr == masks)
{
masks = newstd::ctype_base::mask[std::ctype<char>::table_size];
std::fill_n(masks,std::ctype<char>::table_size, std::ctype_base::mask());
masks[' '] =std::ctype_base::space;
masks[','] =std::ctype_base::space;
}
return masks;
}
};
int main(void)
{
std::string rawStr{"Welcome to the,new,world"};
std::stringstreamss{rawStr};
ss.imbue(std::locale{std::locale{},new StringSplitLocal{}});
std::vector<std::string>strVec{std::istream_iterator<std::string>{ss},
std::istream_iterator<std::string>{}};
std::copy(strVec.begin(),strVec.end(),
std::ostream_iterator<std::string>{std::cout,"\n"});
return 0;
}
设置一个字符串分割本地化,当然进一步可以把需要分割的符号作为参数传入,这样就可以随心所欲的控制了。看这样,任意字符,任意数量,任意分割,代码短小精悍,简直是太美了!Perfect!
需要注意的是,main中new的StringSplitLocal不要自己释放,因为stringstream析构的时候会做这件事情。如果自己释放掉,会导致继承中的纯虚函数调用失败,导致程序崩溃。
继续继续深入
不得不说,我是一个麻烦不断的人。今天解决宽字节字符串时,自然又陷入泥潭之中。说到这,不得不感叹一下标准库的模板特化,您能保持接口一致么?虽说效率会略低一点,可是,可是……模板库不就是求高效么?
对于宽字节,遇到了一个问题,本以为稍微修改一下字符串分割的本地化,就万事大吉了,修改代码如下:
class StringSplitLocal : public std::ctype<TCHAR>
{
public:
StringSplitLocal() :std::ctype<TCHAR>(GetSplitTable()) {}
staticstd::ctype_base::mask const *GetSplitTable()
{
staticstd::ctype_base::mask *masks = nullptr;
if (nullptr == masks)
{
masks = newstd::ctype_base::mask[std::ctype<TCHAR>::table_size];
std::fill_n(masks,std::ctype<TCHAR>::table_size, std::ctype_base::mask());
masks[_T(' ')] =std::ctype_base::space;
masks[_T(',')] =std::ctype_base::space;
}
return masks;
}
};
结果让我大跌眼镜,std::ctype<wchar_t>根本没有table_size,心中顿时万匹草泥马在奔腾啊。是啊,char的时候,table_size很好办呀,不就是8bit吗,256而已,可是wchar_t是16bit,如果全部列出来,确实有点过大。所以,std::ctype对char和wchar_t分别进行了模板特化。
当然,这些都是小问题(大问题在后面),于是乎,看了一下ctype<wchar_t>的特化版,发现,只要重写虚函数do_is,就可以解决流进行字符判断问题,于是写下了以下版本:
class StringSplitLocal : public std::ctype<TCHAR>
{
public:
bool do_is(mask maskVal,TCHAR ch) const
{
returnstd::ctype<wchar_t>::do_is(maskVal, ch) || (ch == _T(','));
}
};
心想,这代码越写越精短了,可把我给乐坏了。可是大问题来了,切到char版本,测试一下,心中的草泥马又疯狂的奔过,居然结果不对?!
好吧,看了一下std::ctype对char版本的特化,居然根本没有do_is的虚函数,一切都在is函数中使用256的表进行处理。
最终结果,字符串分割本地化只好也分别针对char和wchar_t进行特化了。不过和写一个字符串分割函数相比,简单是轻松无比!
进一步思考
第一条,还是想标准库把std::ctype保持统一化,虽然我没有针对性能效率兼容性等等各个方面进行测试,但是分别特化实在是让人很不爽。对于我来说,有时候有些东西就应该痛痛快快的砍掉,例如说std::vector<bool>,残留了这样一些不三不四的东西,终究会阻碍C++的发展。
第二条,越来越发现C++的地大物博,像是满天的繁星,而我却想将它数清。坚定信念,继续努力,发现更多更多,学习更多更多,让激动不断充满心中!
第三条,没有充分测试,有问题的小伙伴要随时反馈啊~~~