第2条 字符串格式化的"动物庄园"之一: 难度系数:3
sprintf
在本条及下一条中,我们将对sprintf的是是非非进行一次奥威尔 式的严格考察,并指出为什么说其他替代方案总是(对,总是)比sprintf好。
初级问题
1. 什么是sprintf?尽可能多地列举出sprintf的替代方案。
专家级问题
2. sprintf的主要优势跟弱点分别是什么?请明确加以说明。
解决方案
"所有动物都是平等的,但其中有些动物比其他动物更'平等'。"
--乔治·奥威尔,《动物庄园》
1. 什么是sprintf?尽可能多地列举出sprintf的替代方案。
考虑如下的C代码,它使用sprintf将一个整型值转化为可读的字符串形式,之所以要这样做可能是为了将一个整型输出到报表或者打印到GUI窗口上:
- // 示例2-1:在C里面使用sprintf来字符串化某些数据
- // PrettyFormat()接受一个整型为参数,将它格式化并
- // 放入给定的输出缓冲区当中
- // 出于格式化的考虑,格式化的结果必须至少为4个字符宽
- //
- void PrettyFormat(int i, char* buf) {
- // 代码就这些,优雅、简洁
- sprintf(buf, "%4d", i);
- }
大奖问题是,在C++(www.cppentry.com)中应该如何完成这件事情呢?
呃,好吧,其实问题不该这样问,毕竟示例2-1也是合法的C++(www.cppentry.com)代码。真正的大奖问题是:抛开C++(www.cppentry.com)标准[C++(www.cppentry.com)03]从C标准[C99]那儿承袭来的桎梏和局限性(如果它们的确是桎梏的话),是不是有办法借助于C++(www.cppentry.com)中的类和模板等特性来将这件事做得更好呢?
问题在这里开始变得有趣起来,因为实现这一目的,至少有不下4种截然不同的、直截了当的标准做法,示例2-1是其中的第一种。其中任一种都提供了在清晰性、类型安全性、运行时安全性以及效率之间的权衡。此外,套用乔治·奥威尔小说中的那只修正主义的猪的名言:"所有这4种选择都是标准的,但其中有些比其他选择要'更标准'一些。"而且,说得更严重一些,它们并非全都基于同一个标准。它们分别是(后面将按照此顺序讨论):
sprintf [C99, C++(www.cppentry.com)03]
snprintf [C99]
std::stringstream [C++(www.cppentry.com)03]
std::strstream [C++(www.cppentry.com)03]
除此之外,还有另一个"目前虽不合标准但很有希望成为标准"的替代方案,好像嫌手头的方案还不够多似的,它就是:
boost::lexical_cast [Boost]
boost::lexical_cast主要用在不需要任何特殊格式化的简单转换当中。
好了,闲话少说,言归正传。
sprintf()的悲与乐
2. sprintf的主要优势跟弱点分别是什么?请明确加以说明。
示例2-1中的代码只是使用sprintf的众多可能的方式中的一种。我们用示例2-1来引发下文的讨论,不过不要过分依赖于这个简单得只有一行代码的PrettyFormat函数。要记住我们的大方向:我们的兴趣在于通常情况下如何将非字符串的值格式化为字符串形式,或许在实际编码当中,我们的做法是在不断变化和改进着的,而不像示例2-1当中的一行简单代码那样。
下面我们将更详细地分析sprintf(),并列出其中存在的主要问题。sprintf()有两个主要的优势,还有3个明显的缺陷。其中两个优势如下。
议题#1:易用性与清晰性。一旦你学会了sprintf的常用格式化标志以及它们的各种组合,其使用就会变得简洁明了,没有任何拐弯抹角之处。使用sprintf的代码明白无误地说明了它正在做的事情。因此printf家族在大多数文本格式化场合下是很难有功能能够与之匹敌的。(确实,我们中的大部分人有时仍然免不了需要去查寻一些不常用的标志,不过它们毕竟用得很少。)
议题#2:效率最佳(能够直接利用现有的缓冲区)。通过使用sprintf将结果直接放入一个已有的缓冲区中,PrettyFormat()将不用牵涉任何动态内存分配或者其他额外的幕后操作就能完成任务。将一块已分配好用于存放输出结果的缓冲区传递给PrettyFormat(),后者负责将格式化的结果直接写入这块缓冲区。
告诫 当然,现在也不必过分在乎效率,因为你的应用程序或许根本就不会在意这一点效率差别。永远不要过早进行优化,只有当时间测试显示确实有必要时才去进行优化。而且,遇到这种情况的时候,永远不要忘记,效率是以牺牲内存管理封装性而换取的。议题#2等于是在说:"你自己去管理内存。"不过别忘了,这句话换一种说法就是"你得自己管理内存"!
只可惜,正如大多数使用sprintf的程序员所知道的,情况还远远不止这些。sprintf同样存在一些显著的缺陷。
议题#3:长度安全性。sprintf是引起缓冲区溢出错误的原因之一,如果目标缓冲区碰巧不够大,装不下整个输出结果,就会发生缓冲区溢出 。例如,考虑如下的代码:
- char smallBuf[5];
- int value = 42;
- PrettyFormat(value, buf); // 呃……隐患
- assert(value == 42);