《C++编程剖析:问题、方案和设计准则》——1.2字符串格式化的“动物庄园”之一:sprintf...

本节书摘来自异步社区出版社《C++编程剖析:问题、方案和设计准则》一书中的第1章,第1.2节,作者:【美】Herb Sutter(赫布 萨特),更多章节内容可以访问云栖社区“异步社区”公众号查看。

1.2字符串格式化的“动物庄园”之一:sprintf

难度系数:3

在本条及下一条中,我们将对sprintf的是是非非进行一次奥威尔[1]式的严格考察,并指出为什么说其他替代方案总是(对,总是)比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++中应该如何完成这件事情呢?

呃,好吧,其实问题不该这样问,毕竟示例2-1也是合法的C++代码。真正的大奖问题是:抛开C++标准[C++03]从C标准[C99]那儿承袭来的桎梏和局限性(如果它们的确是桎梏的话),是不是有办法借助于C++中的类和模板等特性来将这件事做得更好呢?

问题在这里开始变得有趣起来,因为实现这一目的,至少有不下4种截然不同的、直截了当的标准做法,示例2-1是其中的第一种。其中任一种都提供了在清晰性、类型安全性、运行时安全性以及效率之间的权衡。此外,套用乔治·奥威尔小说中的那只修正主义的猪的名言:“所有这4种选择都是标准的,但其中有些比其他选择要‘更标准’一些。”而且,说得更严重一些,它们并非全都基于同一个标准。它们分别是(后面将按照此顺序讨论):

sprintf [C99, C++03]
snprintf [C99]
std::stringstream [C++03]
std::strstream [C++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是引起缓冲区溢出错误的原因之一,如果目标缓冲区碰巧不够大,装不下整个输出结果,就会发生缓冲区溢出[2]。例如,考虑如下的代码:

char smallBuf[5];
int value = 42;
PrettyFormat(value, buf);       // 呃……隐患
assert(value == 42);`
本例中,42恰好足够小,以至于5B大小的结果“ 420”恰巧能够放在smallBuf中。然而,设想某一天代码改成了这样:

char smallBuf[5];
int value = 12108642;
PrettyFormat(value, buf);       // 哦!
assert(value == 12108642);     // 这个断言很可能会失败!```
这会导致smallBuf尾部之后的区域也被改写,而倘若编译器恰巧让value(在内存中)紧跟在smallBuf之后的话,被改写的区域就是value值本身占用的空间了!

我们无法轻易地改善示例2-1的安全性。的确,我们可以让PrettyFormat()接受缓冲区的长度并对sprintf()的返回值进行检查,但这等于是事后诸葛亮。具体做法如下:

// 糟糕的主意:丝毫没有改观
//
void PrettyFormat(int i, char* buf, int buflen) {
if(buflen <= sprintf(buf, "%4d", i)) {  // 并不比以前好
 // ……现在情况如何呢?既然在这里问题被侦测出来了,那么这就
 // 意味着问题已经发生了,换句话说该被破坏的内存已经被破坏了
 }
}`
对于这个问题,根本没有解决方案。当错误被侦测出来时,内存已然被破坏,我们已经在不该写的地方写下了一些字节,如果情况糟糕的话,程序甚至根本没机会运行到报错代码处[3]。

议题#4:类型安全性。对于sprintf来说,类型错误就意味着运行时错误,而非编译期错误,更可怕的是这些类型错误甚至根本就不会表现出来。printf家族使用C的可变参数列表,C编译器通常并不检查这类实参列表的类型[4]。几乎每个C程序员都曾在一些微妙的或者不那么微妙的情况下发现他们搞错了格式字符串,这类错误总是再频繁不过地发生着,譬如在熬夜调试之后,试图重现某个关键客户遇到的神秘崩溃问题时。

诚然,示例2-1中的代码非常简单,只要清楚地知道我们只是将一个int传给sprintf,就可能足够简单地维护它。不过,即便如此,事情仍然可能出现纰漏,设想你的手指一不小心按错了键,这类情况并不罕见。例如,在大多数键盘上,c键跟d键是相邻的,所以我们可能一不小心把d错打成了c,结果就成了这样:

sprintf(buf, "%", i);     // 哦!
这会导致输出结果为字符而不是数字,这种情况下我们或许很快就能意识到错误所在,因为sprintf会一声不吭地将i的第一个字节解释为一个char值。此外,s键也跟d键相邻,因此如果我们错误地写成了:

sprintf(buf, "%4s", i);     // 糟糕!
如果情况是这样的话,或许我们同样能够很快反应过来,因为这么做很可能会令程序立即崩溃或至少偶发性地崩溃。因为这时sprintf会不加提示地将i解释为指向字符串的指针,并欣然地顺着这个指针所指的方向去寻找一个实际上并不存在的字符串,实际上,这个指针可能指向内存中的任何位置。

不过,下面这种情况可就微妙了,假设我们将d错打成了ld,会出现什么情况呢?

sprintf(buf, "%4ld", i);    // 一个微妙的错误
若是这种情况的话,给出的格式字符串就等于是在告诉sprintf,给它的是long int,而实际上给的却是int。这同样也是糟糕的C代码,不过,问题是它不仅不会以编译期错误的形式表现出来,甚至不会导致运行时错误。在许多流行的平台上,程序的运行结果仍然会跟以前一样,因为int在许多流行平台上碰巧跟long int具有相同的大小和内存布局。因而你也许一直都不会注意到这个潜在的问题,直到某一天将代码移植到某个平台上,该平台上的int跟long int具有不同的大小,这时才发现这个问题,甚至就连这个时候,程序可能也并不总是产生错误的结果或立即崩溃。

最后,考虑一个与此有关的问题。

议题#5:模板亲和性。很难将sprintf放在一个模板当中。考虑如下的代码:

template<typename T>
void PrettyFormat(T value, char* buf) {
 sprintf(buf, "%/* 这里应该写些什么呢?*/", value);
}```
你所能做到的最好的(最糟的?)就是声明一个主模板,并为所有那些与sprintf兼容的类型分别提供对应的特化版本:

// 不算好点子: 一个东拼西凑出来的PrettyFormat
//
template
void PrettyFormat(T value, char* buf);   // 注意:主模板只有声明,没有定义
template<> void PrettyFormat(int value, char* buf) {
sprintf(buf, "%d", value);
}
template<> void PrettyFormat(char value, char* buf) {
sprintf(buf, "%c", value);
}
//……还有其他特化版本,呃……`

已标记关键词 清除标记
表情包
插入表情
评论将由博主筛选后显示,对所有人可见 | 还能输入1000个字符
相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页