snprintf、stringstream、strstream的对比分析

这两天在看《C++编程思想》IO部分,里面有讲到strstream的使用,但在g++里却编译不通。上网一查才发现strstream已经被stringstream替代了,后者在类型转换中有巧妙的应用。但这二者差别是很大的。

sprintf问题的奥威尔式的严格考察,最终以我们对snprintfstd::stringstreamstd::strstream以及非标准但极度优雅的boost::lexical_cast的一番对比分析结束。

Guru问题

1. 比较下面这些替代方案的优点和弱点,使用第2条中的分析和示例代码。

asnprintf

bstd::stringstream

cstd::strstream

dboost::lexical_cast

解决方案

替代方案#1snprintf

1. 比较下面这些替代方案的优点和弱点,使用第2条中的分析和示例代码。

asnprintf

在所有的选择当中,与sprintf最相近的选择当然是snprintf了。snprintf只是在sprintf上增加了一项功能,不过是一项重要功能,即用户可以给出输出缓冲区的最大长度,从而避免缓冲区溢出。当然,如果缓冲区太小的话,输出结果就会被截断。

长久以来,在大多数C实现上,snprintf都是作为一个非标准的扩展存在的。随着C99标准的颁布[C99]snprintf终于浮上台面而成为合法功能,目前snprintf已经是C99标准中的正式一员。不过,除非你的编译器是符合C99标准的,否则可能仍然必须使用供应商提供的非标准扩展,如_snprintf

坦白地说早该使用snprintf来取代sprintf即使在snprintf还没有标准化之前。大多数良好的编码标准都不推荐你使用像sprintf这样的不检查长度的函数,而且该原则是很有道理的。使用不做检查的sprintf长久以来会引起一些声名狼藉的常见问题,它通常会导致程序崩溃[1],尤其会导致安全脆弱问题[2]

借助于snprintf,我们就可以正确编写刚才一直试图实现的带长度检查的PrettyFormat()版本。

// 示例3-1:在C中使用snprintf来字符串化某些数据

//

void PrettyFormat(int i, char* buf, int buflen) {

 // 这就是代码,简洁优雅,关键是比以前要安全得多:

 snprintf(buf, buflen, "%4d", i);

}

注意,即便这样做了,仍然还存在另一种出错的可能,即调用者将缓冲区长度搞错了。这意味着跟那些具有资源管理功能的替代方案相比,snprintf还算不上百分之百地杜绝缓冲区溢出可能性,不过跟sprintf相比它显然要安全多了,在长度是否安全?这个问题上应该算是合格的。使用sprintf没有合适的途径来绝对避免缓冲区溢出,而通过snprintf,我们则可以(很大程度上)杜绝缓冲区溢出。

注意,snprintf的一些标准化之前版本的行为稍有不同。尤其是在一个主要实现中,如果输出结果填满或者大于缓冲区容量,缓冲区里的串就不会以'\0'结尾。这种情况下,我们的PrettyFormat()函数就得稍作调整以应付这种非标准的行为:

// C中使用一个并不十分遵从C99标准的_snprintf来将数据字符串化

//

void PrettyFormat(int i, char* buf, int buflen) {

 // 这里是代码,简洁优雅,而且安全得多

 if(buflen > 0) {

   _snprintf(buf, buflen-1, "%4d", i);

   buf[buflen-1] = '\0';

 }

}

在其他任何方面sprintfsnprintf都是一样的。综上所述snprintfsprintf的比较如表3-1所示

表3-1  snprintf与sprintf的比较

 

snprintf

sprintf

标准吗

易用吗,代码清晰明确吗

高效吗,无额外的内存分配吗

长度安全吗

类型安全吗

可用于模板之中吗

是:仅[C99],不过也可能进入C++0x

是:[C90],[C++03],[C99]

从这些比较当中,我们可以给出如下的建议:

准则:永远不要使用sprintf

如果你真的决定使用Cstdio设施的话,一定要记住,使用那些进行长度检查的函数,如snprintf。即便在你的编译器上它们只是作为非标准扩展存在,也得使用它们,因为使用它们没有任何损失,还能够带来实实在在的好处。

我曾在C++大会上将该主题作为演讲稿的材料,一开始我便惊讶地发现,通常在一场大会的所有到场人员当中只有百分之十的人听说过snprintf。然而,几乎每次,当我问到关于sprintf在实际项目当中导致的问题时,总会有人立即举手,描述他们最近在项目当中发现一些缓冲区溢出bug,而当他们在整个项目中将sprintf全部替换为snprintf之后再去进行测试,发现不但这些bug消失了,就连其他一些早就报告了的、已经在bug队列里面呆了很长时间却一直没人能够解决的神秘bug也随之消失了。

结论,正如我一直所说的,是永远不要使用sprintf

替代方案#2std::stringstream

bstd::stringstream

C++中用于字符串化的最常见设施就是stringstream这一族的类了。示例3-1如果用ostringstream来替代sprintf的话看起来就会像这样

// 示例3-2C++中进行字符串化使用ostringstream

//

void PrettyFormat(int i, string& s) {

 // 不如原先的简洁优雅

 ostringstream temp;

 temp << setw(4) << i;

 s = temp.str();

}

相对于sprintf来说stringstream具有一些优点但同时也有缺点。在sprintf光芒四射的那些地方stringstream显得并不那么出色。

议题#1:易用性和清晰性。使用stringstream不仅让原先的一行代码变成了三行,而且我们还得引入一个临时变量。使用stringstream的做法有几个优势,不过代码的清晰性并非其中之一。这并不是说像setw(4)这样的流操纵子难于学习,实际上它们跟sprintf的格式化标志一样易学,只不过前者通常更为笨拙冗长一些。我发现那些到处点缀着像<<setprecision(9)<<setw(14)这样的长名字的代码会难于阅读(我是说,跟%14.9这种格式化字符串相比),即便所有的操纵子都整齐排列也无济于事。

议题#2:效率(能否直接利用现有缓冲区)。stringstream会自己另外分配一份单独的缓冲区来存放结果,另外还需要使用一些辅助性的对象,通常所有这些都意味着需要进行额外的内存分配。我在两个当前流行的编译器上测试了示例3-2的代码,同时让::operator new统计总共的分配次数。结果发现在某个平台上有两次动态内存分配,另一个平台上则是三次。

而在sprintf一筹莫展的那些地方stringstream则大显身手

议题#3:长度安全性stringstream内部的basic_stringbuf缓冲区类会根据需要自动增长以便容纳需要存放的数据。

议题#4:类型安全性。使用operator<<和重载决议,即便是对于那些提供了自己的流插入操作符的自定义流类型,也总能够实现类型安全性。不会因为类型不符而导致一些神秘的运行时错误。

议题#5:模板亲和性。既然编译器会自动调用正确的operator<<那么将PrettyFormat泛化为可接受任何类型的数据应当是举手之劳

template<typename T>

void PrettyFormat(T value, string& s) {

 ostringstream temp;

 temp << setw(4) << value;

 s = temp.str();

}

综上所述stringstreamsprintf的比较如表3-2所示。

表3-2  stringstreamsprintf的比较

 

stringstream

sprintf

标准吗

 

易用吗,代码清晰明确吗

高效吗,无额外内存分配吗

长度安全吗

类型安全吗

可用于模板之中吗

是:[C++03]

 

是:[C90],

[C++03],[C99]

替代方案#3std::strstream

cstd::strstream

不管这种说法公平与否strstream都是要被遗弃的。由于[C++03]标准将它标明为deprecated(不赞成的)因而优秀的C++书籍顶多也只是略微提及一下[Josuttis99]的第649),大多数则是根本不提[Stroustrup00]),甚至明确地表态说不会讨论这方面的内容因为strstream是官方规定的替补[Langer00]的第587。标准委员会觉得stringstream可以取代strstream,因为stringstream更好地封装了内存管理,所以他们将strstream标明为deprecated,然而strstream仍然还是标准的法定成员,任何符合C++标准的实现都必须提供它[3]

由于strstream仍然是标准的,所以为了完整起见这里还是提一下它。碰巧它也的确提供了一些优点。使用strstream的话,示例3-1看起来就会像这样:

// 示例3-3C++中使用ostrstream进行字符串化

//

void PrettyFormat(int i, char* buf, int buflen) {

 // 不算太差不过别忘了最后还要输出结束符

 ostrstream temp(buf, buflen);

 temp << setw(4) << i << ends;

}

议题#1:易用性和清晰性strstream在易用性跟代码的清晰性方面略逊stringstream一筹。两者都要求建立一个临时对象,不过strstream要求你记得手动输出一个结束符来结束字符串,这除了令人感觉有点不愉快之外,还有点危险,因为如果一不小心忘记了,同时在读取结果串的时候又期望该串是以'\0'字符结尾的话,你就面临着读取超过结果串末尾之后的内存数据的危险,而就算sprintf也没这么脆弱,它总是会在结果串的末尾加上结束符。不过,按照示例3-3所展示的方式那样使用strstream至少有一个优点,即无需在最后调用c_str()来获取结果串。(当然,如果让strstream创建自己的缓冲区,其内存只是部分封装的,你除了得在最后调用.str()来将其中的结果串取出来之外,还得加上一次.freeze(false)调用,否则strstreambuf在析构的时候是不会释放内存的。)

议题#2:效率(能否直接利用现有缓冲区)。我们只需在创建ostrstream对象的时候传递一个指向现有缓冲区的指针,就可以避免任何额外的内存分配,ostrstream会将它的结果直接输出到该缓冲区当中。这跟stringstream相比是一个非常重要的区别,在能否将结果串直接输出到现有的目标缓冲区(从而避免额外的内存分配)这个问题上,stringstream根本无法与strstream比拟[4]。当然,如果你并没有现成可利用的缓冲区,ostrstream也可以使用自己动态分配的缓冲区,你只需调用它的默认构造函数即可[5]。确实,strstream是我们所讨论的所有可选方案当中惟一能够提供这种选择自由的方案。

议题#3:长度安全性ostrstream内部的strstreambuf缓冲区会自动检查它的长度以确保不会写超过给定缓冲区之外的内存区域。而如果我们使用的是一个默认构造的ostrstream对象的话,其内部的strstreambuf缓冲区就会根据需要自动增长以容纳有待存储的值。

议题#4:类型安全性strstreamstringstream一样完全是类型安全的。

议题#5模板亲和性。完全可以正如stringstream一样。例如

template<typename T>

void PrettyFormat(T value, char* buf, int buflen) {

 ostrstream temp(buf, buflen);

 temp << setw(4) << value << ends;

}

总之strstreamsprintf的比较结果如表3-3

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值