C++中输出十六进制形式的字符串
前言
在进行i18n相关的开发时,经常遇到字符编码转换的错误。这时如果能把相关字符串用十六进制的形式打印出来,例如,"abc"输出成"\\x61\\x62\\x63"这对于i18n的除错来说是很有帮助的。Python里面,只需要使用repr()函数就行了。可在C++中如何做到这点呢?
下面是用ostream的格式化功能的一个简单的实现:
std::stringget_raw_string(std::stringconst&s) { std::ostringstreamout; out<<'\"'; out<<std::hex; for(std::string::const_iteratorit=s.begin();it!=s.end();++it) { out<<"\\x"<<*it; } out<<'\"'; returnout.str(); }
看上去简单直接,但很可惜这段代码不能实现我们的意图。它还是按字面输出了每个字符。可我们明明指定了使用std::hex来格式化输出啊!?问题原来是出在std::hex只是一个针对整数类型的输出格式设置,当输出字符类型时,C++流还是按照字面输出。到ostream的文档去细查才知,原来C++标准输出流对于格式化输出的控制很弱,只能提供有限的几种格式定制,而且大部分都是针对整数和浮点数类型的,对于字符类型完全没有参数可以控制。有点讽刺的是,ostream利用了C++的函数重载和强类型机制做到了在表达力不输于C的同时,又杜绝了臭名昭著的printf带来的无穷的麻烦,大大增加了安全。可在这里,强类型安全反而是我们达到目的的障碍:我就是想让ostream把字符当成整数打印啊!还好,C++还有类型强转这招可以让我们绕过强类型匹配这道安全闸门:
out<<std::hex<<"\\x"<<static_cast<int>(*it);
好了,这下字符都按整数来输出了,而std::hex又指示ostream用十六进制表示去输出整数。问题解决了。且慢,为什么输出UTF-8中文编码的时候会变成这样:
"\xffffffe4\xffffffb8\xffffffad"//get_raw_string("中")
这么多的Fword太影响市容了。能不能把它们去掉?其实原因在于,我们输出的是强制类型转换成int的整形数值,而int是32bit长,所以会多出前面这么多位来。如果要去掉,只要转成8bit的整数不就行了吗。可惜C/C++中没有8bit的整数,你唯一能做到的是
typedefcharint8_t;
可是用这样得来的int8_t去转也还是不行,因为在C++中,typedef并没有产生一个新的类型,而只是定义了一个原来类型的别名。而这个别名是不参与到函数重载的匹配计算当中的。换言之,ostream说了,别以为你披上件int8_t的马甲我就不认识你了,我还是把你当char来输出。此路不通!
那我们就放弃利用ostream了吗?且慢,其实ostream默认是不会输出前面的0的,那只要把最后8bit之前的位都抹成0不就能达到我们的要求了吗。
好了,下面就是无错最终版:
std::stringget_raw_string(std::stringconst&s) { std::ostringstreamout; out<<'\"'; out<<std::hex; for(std::string::const_iteratorit=s.begin();it!=s.end();++it) { //AND0xFFwillremovetheleading"ff"intheoutput, //Sothatwecouldget"\xab"insteadof"\xffab" out<<"\\x"<<(static_cast<short>(*it)&0xff); } out<<'\"'; returnout.str(); }
经历了几番波折,终于成功利用了ostream提供的十六进制输出的功能实现了打印字符串十六进制的功能。其实细究起来,之所以那么绕,还是因为ostream本身在格式化输出控制方面太弱了。进一步的,C++里还有更好的工具做这件事吗?boost::format看起来象是,但它依然不能正确处理我们上面遇到的两难境地。好在,另一个boost库给出了合适的答案:boost::spirit::karma
Karma是boost::spirit库的一部分。大家可能比较熟悉的是用spirit库做parser来解析字符串。而spirit通过Karma提供的功能就恰好相反,它是专门用来将C++数据结构格式化为字符流的。
我们恰好就需要它,下面就是用karma库重写的代码:
template<typenameOutputIterator> boolgenerate_raw(OutputIteratorsink,std::strings) { usingboost::spirit::karma::hex; usingboost::spirit::karma::generate; returngenerate(sink,'\"'<<*("\\x"<<hex)<<'\"',s); } std::stringget_raw_string_k(std::stringconst&s) { std::stringresult; if(!generate_raw(std::back_inserter(result),s)) { throwstd::runtime_error("parseerror"); } returnresult; }
这里面最主要就是利用了karma内置的一个输出模块karam::hex来帮我们完成工作,而这个hex是一个多态的生成器。它不象ostream的类型重载,只能针对某些类型输出hex格式,而是针对所有类型都能输出hex格式,包括char。还有一个优点,代码的表达力更强了,输出的格式完全在一行代码中体现:
//输出格式为"\x61\x62\x63",方便直接贴到python或C++的代码中 '\"'<<*("\\x"<<hex)<<'\"'
如果想要改变输出格式,只需要改这行代码即可,例如:
//输出格式变为"0x610x620x63" '\"'<<*("0x"<<hex<<"")<<'\"'
那么效率方面有没有任何性能损失呢?下面是一段测试代码,分别用两种算法转换相同的字符串:
#include"boost/test/unit_test.hpp" #include"boost/../libs/spirit/optimization/measure.hpp" #include"string.hpp"//Thefunctionfortest staticstd::stringconstmessage="hexoutputperformancetestdata中文"; structusing_karma:test::base { voidbenchmark() { this->val+=get_raw_string_c(message).size(); } }; structusing_ostream:test::base { voidbenchmark() { this->val+=get_raw_string(message).size(); } }; BOOST_AUTO_TEST_CASE(TestStringPerformance) { BOOST_SPIRIT_TEST_BENCHMARK( 100, (using_karma) (using_ostream) ); BOOST_CHECK_NE(0,live_code); }
下面是运行的结果,分别是两种算法需要的时间,值越小越好:
可能出乎意料,大致来说karma比ostream快了一倍。这也与spirit官方给出的性能数据差不多。这里的函数返回值是通过std::string值拷贝返回的,消耗了不少时间,如果纯从格式化输出来说,猜测karma的性能优势只会更大。另一份测试表明,karma应该是C/C++里面你能找到的速度最快的格式化字符流方案了。
对于这么简单的功能来说,这篇文章已经显得太长了,庆幸的是,我们最终还是找到了一个表达力强,性能高的十六进制输出方案。人说好事难双,可C++这门复杂的语言,却经常能找执行飞快又高度抽象的代码方案。只是有些过于复杂了...
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流。