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++这门复杂的语言,却经常能找执行飞快又高度抽象的代码方案。只是有些过于复杂了...
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流。