对sprintf问题的奥威尔式的严格考察,最终以我们对snprintf、std::stringstream、std::strstream以及非标准但极度优雅的boost::lexical_cast的一番对比分析结束。
Guru问题
1. 比较下面这些替代方案的优点和弱点,使用第2条中的分析和示例代码。
(a)snprintf
(b)std::stringstream
(c)std::strstream
(d)boost::lexical_cast
解决方案
替代方案#1:snprintf
1. 比较下面这些替代方案的优点和弱点,使用第2条中的分析和示例代码。
(a)snprintf
在所有的选择当中,与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';
}
}
在其他任何方面,sprintf和snprintf都是一样的。综上所述,snprintf跟sprintf的比较如表3-1所示。
表3-1 snprintf与sprintf的比较
|
|
snprintf |
sprintf |
|
标准吗 易用吗,代码清晰明确吗 高效吗,无额外的内存分配吗 长度安全吗 类型安全吗 可用于模板之中吗 |
是:仅[C99],不过也可能进入C++0x 是 是 是 否 否 |
是:[C90],[C++03],[C99] 是 是 否 否 否 |
从这些比较当中,我们可以给出如下的建议:
准则:永远不要使用sprintf。
如果你真的决定使用C的stdio设施的话,一定要记住,使用那些进行长度检查的函数,如snprintf。即便在你的编译器上它们只是作为非标准扩展存在,也得使用它们,因为使用它们没有任何损失,还能够带来实实在在的好处。
我曾在C++大会上将该主题作为演讲稿的材料,一开始我便惊讶地发现,通常在一场大会的所有到场人员当中只有百分之十的人听说过snprintf。然而,几乎每次,当我问到关于sprintf在实际项目当中导致的问题时,总会有人立即举手,描述他们最近在项目当中发现一些缓冲区溢出bug,而当他们在整个项目中将sprintf全部替换为snprintf之后再去进行测试,发现不但这些bug消失了,就连其他一些早就报告了的、已经在bug队列里面呆了很长时间却一直没人能够解决的神秘bug也随之消失了。
结论,正如我一直所说的,是永远不要使用sprintf。
替代方案#2:std::stringstream
(b)std::stringstream
C++中用于字符串化的最常见设施就是stringstream这一族的类了。示例3-1如果用ostringstream来替代sprintf的话看起来就会像这样:
// 示例3-2:在C++中进行字符串化,使用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();
}
综上所述,stringstream跟sprintf的比较如表3-2所示。
表3-2 stringstream与sprintf的比较
|
|
stringstream |
sprintf |
|
标准吗
易用吗,代码清晰明确吗 高效吗,无额外内存分配吗 长度安全吗 类型安全吗 可用于模板之中吗 |
是:[C++03]
否 否 是 是 是 |
是:[C90], [C++03],[C99] 是 是 否 否 否 |
替代方案#3:std::strstream
(c)std::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-3:在C++中使用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:类型安全性。strstream跟stringstream一样,完全是类型安全的。
议题#5:模板亲和性。完全可以!正如stringstream一样。例如:
template<typename T>
void PrettyFormat(T value, char* buf, int buflen) {
ostrstream temp(buf, buflen);
temp << setw(4) << value << ends;
}
总之,strstream与sprintf的比较结果如表3-3所示。
表3-3 strstream与sprintf的比较
|
|
strstream |
sprintf |
|
标准吗
易用吗,代码清晰明确吗 高效吗,无额外内存分配吗 长度安全吗 类型安全吗 可用于模板之中吗 |
是:[C++03] 不过标明为deprecated了 否 是 是 是 是 |
是:[C90], [C++03],[C99] 是 是 否 否 否 |
呃……看到一个已被“打入冷宫”的设施在比较中表现如此良好的确让人有几分尴尬,不过有时候现实就是如此。
替代方案#4:boost::lexical_cast
(d)boost::lexical_cast
如果你还没有接触过[Boost]的话,我的建议是,马上去研究它!Boost是C++的一个开源库,主要由C++标准委员会成员编写。其代码经过严格的同行评审,并由专家编写的,遵从C++标准库的风格,同样,其中设施的明确意图就是作为下一代C++标准库的潜在候选子库,因此花些时间去了解它们是完全值得的。此外,从今天开始你就可以完全免费地使用它们了。
Boost库中提供的设施之一就是boost::lexical_cast,它是stringstream的一个易用的包装类。此外Boost中还包括一些其他更为华丽和重要的设施,它们同样在内部借助于流来实现,并提供了更为sprintf式的格式化选择,其中尤为突出的要数boost::format。由于Kevlin Henney写的Boost代码实在是太简洁优雅了,因此我可以完完整整地将它们罗列在下面(删掉了一些为旧的编译器所做的workaround),所以虽说它目前还未被标准化,我也乐于将它介绍给读者:
template<typename Target, typename Source>
Target lexical_cast(Source arg) {
std::stringstream interpreter;
Target result;
if(!(interpreter << arg) || !(interpreter >> result) || !(interpreter >> std::ws).eof())
throw bad_lexical_cast();
return result;
}
注意,lexical_cast的意图并非是想要成为sprintf的直接竞争者。实际上,sprintf比lexical_cast更为通用,而lexical_cast的目的只是为了将数据从一个可流化的类型转换为另一个可流化的类型,因而它与C中的atoi等转换函数以及非标准但广泛使用的itoa等函数的竞争更为直接一些,然而,lexical_cast与我们当前讨论的主题又是如此地接近,以至于不提及它就明显是个疏忽了。
下面就是使用lexical_cast来改造示例3-1后的情形:
// 示例3-4:在C++中使用boost::lexical_cast进行字符串化
//
void PrettyFormat(int i, string& s) {
// 如果这确实恰是你所需要的,那么可以说这是目前最为简洁优雅的做法了
s = lexical_cast<string>(i);
}
议题#1:易用性和清晰性。在所有这些例子当中,使用lexical_cast的代码最为直接地表达了实际意图。
议题#2:效率(能否直接利用现有缓冲区)。由于lexical_cast使用的是stringstream,因此毫不奇怪它需要至少跟stringstream一样多的内存分配次数。在我测试的一个平台上,示例3-4比示例3-2中直接使用stringstream的版本多进行了一次内存分配,而在另一个平台上则没有多出。
跟stringstream一样,在长度安全性、类型安全性以及模板亲和性这些方面,lexical_cast也有优秀的表现。
总之,lexical_cast跟sprintf的比较如表3-4所示。
表3-4 lexical_cast与sprintf的比较
|
|
lexical_cast |
sprintf |
|
标准吗
易用吗,代码清晰明确吗 高效吗,无额外内存分配吗 长度安全吗 类型安全吗 可用于模板之中吗 |
否:有可能成为 [C++0x]的候选 是 否 是 是 是 |
是:[C90], [C++03],[C99] 是 是 否 否 否 |
小结
到目前为止,还有一些问题是我们未曾详细讨论过的,例如,这里所讨论的所有字符串格式化都是针对基于char类型的窄字符串的,而并没有涉及宽字符串。我们也考察了让sprintf、snprintf、strstream直接利用现有缓冲区而带来的性能提升,然而“你自己去管理内存”的另一面就是“你得自己去管理内存”,因而stringstream、strstream还有lexical_cast提供的封装得更好的内存管理可能相当吸引你。(这里并没有打错字,strstream的确是个双面手,具体要看你如何使用它。)
另外,还有一些我们并没有详细讨论的非标准方案。我选择将boost::lexical_cast作为非标准方案的代表是因为它简洁优雅,不过即便是在Boost当中也存在着更为完备更为重量级的方案,特别值得注意的是boost::format,它建立在与这儿提到的stringstream和strstream技术类似的方法基础之上,提供了更自动化的能力来支持sprintf式的格式化。
将这些放到一起,我们可以得到综合比较的表3-5。考虑到我们在判断每种方案的优劣时应考虑的那些方面,很显然这些方案当中没有哪个是在任何情况下都合适的。
表3-5 C/C++字符串格式化方案
|
|
sprintf |
snprintf |
stringstream |
strstream |
boost:: lexical_cast |
|
|
标准吗?(-= n/a) | ||||
|
[C90] |
是 |
否(常见扩展) |
- |
- |
- |
|
[C++03] |
是 |
否(常见扩展) |
是 |
是(但是deprecated) |
否 |
|
[C99] |
是 |
是 |
- |
- |
- |
|
C++0x(推测) |
是 |
很可能 |
是 |
很可能(也可能仍deprecated) |
可能 |

