[转]各显神通的字符串格式器

来源:互联网 发布:中航软件 编辑:程序博客网 时间:2024/05/02 07:02
[声明]:本英文资料源自于Herb Sutter 创建的“Guru of the Week(GOTW)”栏目,“C++ 翻译小组”的翻译作品供学习交流与参考用途,不得用于任何商业用途。未经Herb Sutter同意,不得转载;对于违反以上条款,翻译小组对此不负任何责任;特此声明。

文章来源:http://www.gotw.ca
版权归属:Herb Sutter
译    者:徐波

MILL#19 各显神通的字符串格式器

This article appeared in C/C++ Users Journal, 19(11), November 2001.

 “虽说所有动物都是平等的,但有些动物还是与众不同。”

—乔治.奥威尔, 《动物庄园》 [1]

考虑下列C代码,它用sprintf( )将一个整型值转换为供人阅读的字符串形式,也可能是为了在报告中输出或在GUI窗口中显示。

// 1

// 使用sprintf()C中将一些数据格式化为字符串,

// PrettyFormat()接受一个整型参数,将其格式化,

// 存于输出缓冲区中。为了格式化,结果串至少要有4个字符宽。

void PrettyFormat( int i, char* buf )
{
// 此为代码,极为简洁。
  sprintf( buf, "%4d", i );
}

$64,000的问题[1]:当你在C++中做这类事情时,该如何考虑?

问题的提法也许并不十分恰当,不管怎样,例1中的用法也是合法的C++代码。真正的$64,000的问题是:抛开C90标准的束缚,如果它真的让你束手束脚的话(C90是C++98标准的基础),在C++中是否存在一种优越的方法,充分利用其类和模板等特性,来完成这项任务?

这正是问题有趣之处,因为至少存在四种直接、明显和标准的方法来完成这项任务,而例1的方法正是其中之一。这四种方法在清晰性、类型安全、运行期安全和效率之间各有利弊。更有意思的是,好象是为了与乔治.奥威尔小说中持修正主义思想的猪们(revisionist pigs)相对应,这四个方法都是标准的,但每个都更执著于标准的某个方面。而且,仿佛是在伤口中再撒一把盐,它们并不都出自同一标准。它们以我讨论的顺序为准分列如下:

sprintf() [2], [3], [4]
snprintf() [4]
std::stringstream [3]
std::strstream [3]

最后,似乎还嫌不够,还存在第五种方法,虽非标准,但却是可能成为标准的有力候选方案,能实现简单的转换,而无需进行特别的格式化。

boost::lexical_cast [5]

闲话少说,言归正传。

[选择方案 #1]:sprintf( )的欢乐与泪水

例1的代码只是sprintf( )用法的一种。我打算把它作为讨论的进入点,但不要太拘泥于PrettyFormat( )这个简单的单行函数。脑子里要有这么一幅大的图像:我们的兴趣是,我们正常情况下该如何选择将非字符串的值格式化为字符串,要考虑一般情况,因为许多代码可能比例1这个简单例子更容易随时间发生改变和增长。

我打算通过对sprintf( )进行更深入的分析来列出所涉及的问题。sprintf( )有两个主要的优点和三个明显的缺点。两个优点如下:

[问题 #1]:容易使用,代码清晰。如果你熟悉常用格式化标志以及它们的组合形式,使用sprintf( )简单明了,不会感到费解。它直接而简明地表达出需要表达的东西。基于这个理由,在大多数的文本格式化工作中,printf( )家族是难以被击败的。(当然,大多数人在遇到不常用的格式化标志时,不得不去查参考手册,但这毕竟不太碰到。)

[问题 #2]:效率高(能够直接使用现有的缓冲区)。使用spirntf( ),直接将结果放入已经提供的缓冲区内,PrettyFormat( )无须执行任何动态内存分配或其它额外的分心事即可完成工作。它拥有已经分配好的空间,可直接放置输出和结果。

要时才这样做。把清晰的代码摆在第一位,只有当以后需要时才考虑速度。在此,要谨记效率的代价是影响了内存管理的封装性。在这里,问题#2说得好听点是“你使用自己的内存管理策略”,说得难听点是“你必须自己进行内存管理”!

唉!正如大多数sprintf( )用户所知,故事并未完美结束。sprintf( )也存在几个显著的缺陷:

[问题 #3]:长度安全。使用sprintf( )是引起缓冲区溢出错误的一个常见根源,只要缓冲区的大小无法满足整个输出[2]的需要。例如,考虑下列代码:

char buf[5];
int value = 42;
PrettyFormat( value, buf ); //
嗯,还能应付。
assert( value == 42 );

在上面这个例子里,42这个值恰好不太大,5个字节的" 42/0"输出结果 正好能放入这个小缓冲区内。但或许有一天,该代码会改变如下:

char buf[5];
int value = 12108642;
PrettyFormat( value, buf ); /
糟!
assert( value == 12108642 ); // 估计会引起失败。

我们将开始在该小缓冲区之外涂鸦,可能会覆盖一些有价值的数据,如果编译器恰好选择了将数据直接放置于小缓冲区之后的内存布局的话。

但是,把例1做得更安全些却非易事。确实,我们可以修改例1,取得缓冲区的长度,检查sprintf( )的返回值,它会告诉我们sprintf( )最终要写入多少字节。代码大致如下:

// 不好: 并没有改进多少的PrettyFormat()
//
void PrettyFormat( int i, char* buf, int buflen )
{
    // 这并无多大改进:
  if( buflen <= sprintf( buf, "%4d", i ) )
  {

    // 唔,怎么办?当检测到问题时,
    // 我们已经把该被破坏的东西破坏掉了。

  }
}

这根本不是解决的办法。当检测到错误时,越界行为早已发生,我们也已经在别人的字节上涂鸦。在更坏的情况下,我们甚至永远执行不了错误报告代码。[3]

[问题 #4]:类型安全。对sprintf( )而言,类型错误属于运行时错误,而非编译时错误,它们甚至可能无法立即显示。printf( )家族使用C的可变参数列表,C编译器一般不会对这类列表进行参数类型检查。[4] 几乎每一个C程序员都有过通过微妙的或不怎么微妙的途径遇到格式标志符错误的“快乐”,几乎所有这些错误都是在重大压力下经过彻夜调试,试图重现一个由关键用户所报告的诡秘的崩溃才被发现的。

当然,例1中的代码非常简单,当我们知道自已正用sprintf( )格式化一个简单的int时,应该是很容易维护的。但即使简单如这种情况,一旦你的手指恰好误击了“d”以外的其它地方,也很容易产生错误。例如,在大多数的键盘上,“c”紧邻“d”,如果我们一不留神,把sprintf( )的调用形式打成下面这个样子:

sprintf( buf, "%4c", i ); //太糟了!

这样,当输出结果是一些字符而不是数字时,我们可能很快就发现这个错误,因为sprintf( )会悄无声息地将i的第一个字节转换成一个字符值。另外,“s”也很靠近“d”,如果错误地输入为:

sprintf( buf, "%4s", i ); //又糟了!

由于程序可能立即崩溃,至少是间歇地发生,所以我们仍可能很快地发现这个错误。在这里,sprintf( )悄悄地将整型转换为一个指向字符的指针,然后兴高采烈地试图将该指针指向内存的任意区域。

还有一个更微妙的情况:如果我们把“d”误输入为“ld”,又会出现什么情况呢?

sprintf( buf, "%4ld", i ); //又糟了!

这时,格式串告诉sprintf( )需要把一个长整型作为数据的第一部分来格式化。这也是一种很差的C代码,但麻烦在于:这不仅不是一个编译时错误,甚至可能不会立即引起运行期错误。在许多流行的平台上,结果仍会与最初的代码相同。为什么?因为在许多流行平台上整型恰好与长整型长度相同,格式也一样。你可能注意不到这个错误,直到你把上述代码移植到整型与长整型长度不一样的平台上。但即使如此,它也并不一定产生错误的输出或立即崩溃。

最后,考虑一个相关的问题。

问题 #5]:可模板化。在模板中使用sprintf( )非常困难。考虑一下:

template<typename T>
void PrettyFormat( T value, char* buf )
{
    sprintf( buf, "%/*这里会是什么?*/", value );

你能采取的最好的方法(也可能是最坏)是声明一个基模板,并为所有与sprintf( )兼容的类型提供特型:

// 不好!: 模板形式的PrettyFormat()之大杂烩。
//
template<typename T>
void PrettyFormat( T value, char* buf );
    // 注意: 基模板尚未定义。

template<>
void PrettyFormat<int>( int value, char* buf );
{
  sprintf( buf, "%d", value );
}

template<>
void PrettyFormat<char>( char value, char* buf )
{
  sprintf( buf, "%c", value );
}

// ... 等等, 嗯 ...

对sprintf( )总结如下:

是否标准?

是: C90, C++98, C99

使用容易吗? 代码清晰否?

效率高,无须额外内存分配?

长度安全吗?

类型安全吗?

能在模板中使用吗?

[选择方案 #2]:snprintf( )

在其它几个选择中,snprintf( )自然与sprintf( )关系最为密切。snprintf( )只是在sprintf( )中增加了一个功能,但却是一个很重要的功能:能够确定输出缓冲区的最大长度,从而避免了缓冲区溢出。当然,如果缓冲区太小,输出结果将被截断。

C的一些主要实现中,大多数长期以来都将snprintf( )作为非标准的扩展形式广泛应用。随着C99标准的出台[4],snprintf( )摇身一变,成为官方认可的标准方法。但除非你的编译器符合C99标准,否则,你不得不以销售商确定的扩展名(诸如_snprintf( )之类)来使用它。

坦白地说,你无论如何应以snprintf( )来替代sprintf( ),即使是在snprintf( )成为标准前。调用象sprintf( )这样不检查长度的函数在大多数良好代码规范中都是禁止的,理由也很充分。对未经检查的sprintf( )的使用长期以来在一般情况下[5]都是臭名昭著的麻烦制造者,特别情况下[6]会导致脆弱的安全性。

使用snprintf( ),我们可以正确地创建能检查长度的版本,前面我们曾试图达到这个目的。

// 2:
//
C中将一些数据格式化为字符串,
// 使用snprintf()
//
void PrettyFormat( int i, char* buf, int buflen )
{
    // 代码如下,非常简洁,
    // 而且安全多了:
  snprintf( buf, buflen, "%4d", i );
}

注意,这样仍无法让调用者避免缓冲区长度错误。这意味着snprintf( )不能百分之百地保证溢出安全性(后面将提到的几个方案可通过封装自己的资源管理方法来做到这一点)。但它毕竟安全多了,问它“是否保证长度安全?”时,可理直气壮地回答“是!”。使用sprintf( ),我们没有合适的方法来避免缓冲区溢出的可能性,但使用snprintf( ),我们可以保证不会发生这种事情。

注意有些早于标准的snprintf( )版本表现稍有不同。特别是在一个主要的实现中,当输出结果大于等于缓冲区长度时,会导致缓冲区不是以’/0’结尾。在这个环境里,函数就要稍作改动,以适应非标准的行为:

// 2(a):
//
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

是否标准?

是:目前仅限C99 ,将来C++0x标准可能也会。

是: C90, C++98, C99

使用容易吗?代码清晰否?

效率高,无需额外内存分配?

长度安全吗?

类型安全吗?

能在模板中使用吗?

[指导方针]:永远不要使用sprintf( )。如果你决定使用C的标准工具,请始终使用象snprintf( )这样能进行长度检测的函数,即使在你的编译器中它只有非标准的扩展形式。换用sprintf( )并没带来新的缺陷,却有实在的好处。

当我把这份材料作为今年(2001)夏天在波士顿举行的Software Development East的部分议题时,我被深深震惊了,班上竟然只有10%的人听说过snprintf( )。但其中一个立即举手表示知道的人描述说,在他目前的工程里,他们刚刚发现了一些缓冲区溢出的bug,通过在工程中用snprintf( )全程替换sprintf( ),在接下来的测试中,不仅那些bug消失了,而且另外几个神秘的bug也突然不见了。要知道,这几个bug几年前就已经被发现了,但开发小组却始终无法诊断出是什么原因。正如我所说的:永远不要使用sprintf( )。

[选择方案 #3]:std::stringstream

C++中将一些数据格式化为字符串的更普遍的做法是使用stringstream家族系列,这儿与示例子看来起很相似,使用ostringstream代替sprintf():

// 3:
//
C++中将一些数据格式化为字符串,
// 使用ostringstream
//
//
void PrettyFormat( int i, string& s )
{
    // 看上去不是那么简单明了:
  ostringstream temp;
  temp << setw(4) << i;
  s = temp.str();
}

使用stringstream的优缺点正好与sprintf( )相反,在sprintf( )闪耀光芒的地方,stringstream却表现得不太好:

[问题#1]:容易使用,代码清晰。不仅原先一行的代码现在要用三行来表示,而且需要引进一个临时变量。这个版本的代码有几个优越的地方,但代码清晰显然不是其中之一。并不是说这些操作器很难学习,其实它们就象sprintf( )的格式标志一样容易学习,但它们看上去显得笨拙而啰嗦。我发现代码中若充斥着象<<setprecision(9)和<<setw(14)这样的长名字,对于读者是个负担(与%14.9相比较而言),即使所有操作器都被很合理地按列排列好。

[问题#2]:效率高(能够直接使用现存的缓冲区)。stringstream在另外的附加缓冲区上工作,所以通常需要执行额外的内存分配,用于创建该工作缓冲区以及其它需要使用的帮助对象。 我将例3的代码在两个当前流行的编译器中作为测试,并利用::operator new( )来计算内存分配的次数。其中一个平台执行了3次动态内存分配,另一个执行了2次。

在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( ):

 

stringstream

sprintf

是否标准?

是: C++98

是: C90, C++98, C99

容易使用吗?代码清晰否?

效率高,无需额外内存分配?

长度安全吗?

类型安全吗?

能在模板中使用吗?

[选择方案 #4]:std::strstream

不知是否公平,strstream仿佛是受迫害的贱民。因为它在C++98标准中属于deprecated(不提倡、反对)的类型,在顶级的C++书籍中,最多也就对它一笔带过[7],大多数对它视而不见,或干脆堂而皇之地声明将不介绍它,因为它被官方宣称是deprecated。尽管标准委员会把它列入deprecated之列,因为他们认为stringstream更好地封装了内存管理,完全可以取而代之,但strstream仍保留在标准中,所有符合C++标准的编译器都必须提供它。[7]

由于strstream仍属标准之列,为了完整起见,这里应该提到它。它偶尔也能提供一些有用的混合的好处。下面是用strstream编写的例1的代码:

// 4:
//
C++把一些数据格式化为字符串,
// 使用ostrstream
//
void PrettyFormat( int i, char* buf, int buflen )
{
    // 不算太差,只是不要忘了加上ends
  ostrstream temp( buf, buflen );
  temp << setw(4) << i << ends;
}

[问题#1]:容易使用,代码清晰。strstream在容易使用和代码清晰方面比stringstream还要稍差一点。它们两个都需要构建一个临时对象。使用strstream,你必须记住在字符串的结尾处要加上一个ends来结束字符串,这很令我讨厌。如果你忘了这样做,以后当你读入它时,就处于溢出缓冲区的危险之中。如果你不得不依靠一个null字符来确认它的结束,即使sprintf( )也没有这般脆弱,始终要靠个null来保证安全。但是,至少以例4所示的方式使用strstream不需要在最后调用.str( )函数来提取结果。(当然,你也可以让strstream创建自己的缓冲区。但这样内存就只是部分被封装;你不仅最终需要一个.str( )的调用来输出结果,而且需要一个.freeze(false)调用来释放strstreambuf占用的内存。)

[问题#2]:效率高(能够直接使用现存的缓冲区)。通过在现存的缓冲区内用一个指针来构建ostrstream对象,根本无需执行任何的动态内存分配;ostrstream将把自己的结果直接送入输出缓冲区。这也是它与stringstream的重要区别,提供了无比伦比的方便办法,将结果直接送到现存的目标缓冲区,从而避免了额外的内存分配[8]。当然,如果你没有现成的缓冲区,ostream也可以使用它自己动态分配的缓冲区,你只要使用ostrsteam的缺省构造函数即可[9]。事实上,strstream也是本文所讨论的备选方案中唯一能让你这样选择的。

[问题#3]:长度安全。正如例4所示,ostrstream的内部strstreambuf缓冲区自动检查自己的长度,保证不写到所提供的缓冲区的外面。如果我们采用了缺省构造的ostrstream,其内部的strstreambuf缓冲区将根据需要自动扩展,以适应需要存储的值。

[问题#4]:类型安全。绝对的类型安全,和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( ):

 

strstream

sprintf

是否标准?

是: C++98

是: C90, C++98, C99

容易使用吗?代码清晰否?

效率高,无需额外内存分配?

长度安全吗?

类型安全吗?

能在模板中使用吗?

唉!看上去实在有些尴尬。逐项对比,这个deprecated方法表现挺好,可生活就是这样。

[选择方案 #5]:boost::lexical_cast

如果你尚未通过http://www.boost.org/ 了解Boost,那我建议你去看一下。它是一个有关C++工具的公共库,主要由C++标准委员会的成员所编写。它不仅是一种由专家所编写的值得好好品味的代码,其风格也沿袭了C++标准库,而且这些工具的明确意图就是要成为下一个C++标准扩展的候选对象,因此值得去了解。除此之外,目前你可以自由地使用它们。

Boost库提供的其中一个工具就是boost::lexical_cast,它是对stringstream的一种方便的包装。Kevlin Henney的代码是如此的简洁和优雅,我几乎可以在这儿就能把它全部展现出来(除去一些为旧式编译器所做的细琐工作)。

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( )的直接竞争对手,lexical_cast是为了将数据从一种可流化类型(streamalbe type)转换为另一种,它更多地是与C的atoi( ) et al.转换函数以及非标准但普遍应用的itoa( ) et al.转换函数直接竞争。它们非常接近,以致我若不在这里提到它,肯定会是一种遗漏。

下面是用lexical_cast对例1进行改写后的样子,减少了至少四个字节的需求:

// 5:
//
C++中将一些数据格式化为字符串,
// 使用boost::lexical_cast
//
void PrettyFormat( int i, string& s )
{
    // 可能是最清楚和最简单的,
    // 如果它能满足你所有需要的话。
  s = lexical_cast<string>( i );
}

[问题#1]:容易使用,代码清晰。这个代码体现了所有这些例子中绝大多数的意图的直接表达。

[问题#2]:效率高(能够直接使用现存的缓冲区)。因为lexical_cast使用了stringstream,所以毫不惊奇,它至少需要和stringstream一样多的内存分配。在我测试的其中一个平台上,例5的代码比使用普遍stringstream的例3要多执行一次内存分配;在另一个平台上,它执行的内存分配次数与普遍stringstream没有差别。

和stringstream一样,在长度安全、类型安全和可模板化方面,lexical_cast表现非常出色。

作为总结,这里将lexical_cast 与 sprintf()作了对比:

 

lexical_cast

sprintf

是否标准?

是/ 否 (但可能被C++0X标准所接纳)

是: C90, C++98, C99

容易使用吗?代码清晰否?

效率高,无需额外内存分配?

长度安全吗?

类型安全吗?

能在模板中使用吗?

[总结]

有些地方我们还没有仔细研究。例如,这里所要格式化的字符串都是普通的基于窄字符的字符串,而非宽字符串。我们强调了通过直接使用现有的缓冲区(如sprintf( ),snprintf( )和strstream)来获得高效率的能力,但“你使用自己的内存管理方式”的另一面却是“你不得不自己负责内存管理”,而提供了更好的内存管理封装的stringstream、strstream和lexical_cast却可能束缚你的手脚。(不要奇怪,strstream两边都出现了,这要看你想怎样使用它。)

总而括之,表1通过逐项对比作了总结。考虑到各个方案都有其相对优点,所以很难断定哪个方案能全面压倒对手。

通过表1,我提取了下列的指导方针,总结于表2:

?如果你只是想把一个值转换为字符串(或其它东西),最好把boost::lexical_cast作为缺省方案。

?对于简单的格式化,或者你需要宽字符或想让它能用于模板,推荐strstream或strstream,较之snprintf( ),它们的代码要啰嗦些,可读性也差点,但对于简单的格式化还不至于太糟。

?对于更完全的格式化,而且你不需要宽字符,也不关心模板,那么snprintf( )是上上之选。不要以为它属于C语言就不适合C++程序员使用。

?如果实际执行效率测试显示上面几种方案都嫌低效,成了你代码中某个特定地方的瓶颈,只有在这种情况下,才考虑使用strstream或snprintf( )中速度较快的一个。

?永远不要使用sprintf( )。

最后,对地位低下的strstream再说一句:它提供了一些有趣的特性组合,并不仅仅因为它是所有方案中唯一允许你选择自己的内存管理方法或让对象(部分地)封装它。它仅有的技术上的缺陷是由于ends这个问题以及内存管理方法使它多少显得有些脆弱。另外它仅有的缺点是它的地位低下,因为它已被晾在一边,不属提倡之列。说得更严重一点,你应该考虑到这样一种可能性(虽然可能性不大),以后的C++标准和你的编译器/库销售商会在将来的某个时候把它抛弃。

一种各方面表现都不错的方法却不被人欣赏,确实有点奇怪。尽管一种动物可能有其独特的优点,即使以“有些动物更胜人一筹”这个标准来衡量。

 

  sprintf() snprintf() std::stringstream std::strstream boost::lexical_cast 是否标准?(--=n/a)           C90 是 否(只是公共扩展) -- -- -- C++98 是 否(只是公共扩展) 是 是,but deprecated 否 C99 是 是 -- -- -- C++0x(将来) 是 有可能 是 可能,但有可能被删除 可能 使用容易吗? 代码清晰否? 效率高,无须额外内存分配? 长度安全吗? 类型安全吗? 能在模板中使用吗? sample timings,normalized to sprintf();           Borland BC++ 5.5.1/windows ME 1.0 1.0 12.6 8.1 19.7 Gnu g++ 2.952/Cygwin+Windows Me 1.0 -- -- 2.0 -- Microsoft VC7 beata/windows Me 1.0 1.0 13.2 9.0 19.2 Rogue Wave SourcePro Core C++ Standard Library("RW")2.1.1/sunPro5.3/sunOS 5.7 1.0 1.1 8.7 4.7 16.5 RW 2.2.1/HP aCC 3.30/HP-UX 11.00 1.0 1.0 7.9 3.9 9.9

表1: C 和C++的各种字符串格式化方案 

  缺省情况下,效率并不特别重要 效率非常重要的情况下 如果你只想把数据转换为字符串形式 boot::lexical_cast std::strstream或snprintf( ) 简单的格式化,或你需要宽字符或

想让它能用于模板。 std::stringstream

或std::strstream std::strstream 或snprintf( )
更完全的格式化,且无需宽字符,也不想使用模板。 snprintf( ) snprintf( )

表2:指导方针概括

[致谢]

感谢Jim Hyslop和ACCU关于sprintf( )讨论的参与者们,你们使我想到这方面的主题。感谢Rogue Wave Software的Martin Sebor提供了非windows平台的时间测试结果。感谢Bjarne Stroustrup,Scott Meyers, Kevlin Henney和Chunk Allison,他们对本文的草案作了评论。

References

[1] George Orwell. Animal Farm (Signet Classic, 1996, ISBN 0451526341).

[2] ISO/IEC 9899:1990(E), Programming Languages - C (ISO C90 and ANSI C89 standard).

[3] ISO/IEC 14882:1998(E), Programming Languages - C++ (ISO and ANSI C++ standard).

[4] ISO/IEC 9899:1999(E), Programming Languages - C (ISO and ANSI C99 standard).

[5] Kevlin Henney. C++ BOOST lexical_cast, http://www.boost.org/libs/conversion/lexical_cast.htm.

[6] Bjarne Stroustrup. "Learning Standard C++ as a New Language" (C/C++ Users Journal, 17(5), May 1999).

[7] Nicolai Josuttis. The C++ Standard Library (Addison-Wesley, 1999), page 649.

[8] Bjarne Stroustrup. The C++ Programming Language, Special Edition (Addison-Wesley, 2000), page 656.

[9] Angelika Langer and Klaus Kreft. Standard C++ IOStreams and Locales (Addison-Wesley, 2000), page 587.

[注释]

1. 要知道,在这个词刚刚出现的那个年代,64,000美元足可以买些房子,安享晚年。

2. 初学者常见的一个错误是依赖宽度修饰符保证长度安全(在这里是“4”),但这样做没什么用处,因为宽度修饰符规定了最小的宽度,而不是最大的宽度。

3. 注意有些场合你可以通过在运行期创建自己的格式器来缓解缓冲区长度问题。Bjarne Stroustrup在[6]中提到了类似的情形:

专家级别的选择方案,我不想对新手详加解释。

char fmt[10];

// 创建一个格式化字符串:普遍的%s可导致溢出。
sprintf(fmt,"%%%ds",max-1);

// name中最多读入max-1个字符。
scanf(fmt,name);

 

4.使用类似lint的工具有助于捕捉这种类型的错误。

5.这是一个现实的问题,不仅仅是sprintf( ),C库中所有长度未检测的函数都存在这个问题。正如John Nagle在2001年9月17日的news:comp.lang.c++.moderated中所概括的“点击这里打开所有的提及“strcpy”和“缓冲区溢出”的网页”。

http://www.google.com/search?q=strcpy+%22buffer+overflow%22 顶级的条目如:

"rdist" buffer overflow exploit
RSA Labs: "especially vulnerable"
Buffer overflow in delivermail
Samba remote buffer overflow
[SECURITY] Buffer overflow in nnrpd
Buffer overflow in php.cgi
wu-ftpd potential buffer overflow
linux-kernel unexecutable stack buffer overflow exploits
cannaserver remote buffer overflow
DeleGate multiple buffer overflow
mSQL buffer overflow advisory
..."

6.例如,多年来被居心险恶的web服务器所推崇的web浏览器破坏手法就是给它们发送非常长的URL,超过web浏览器内部URL字符串缓冲区。那些不检查长度就将其拷贝到固定长度的缓冲区的web浏览器就会使字串写到缓冲区外面,通常覆盖数据区,有时也会覆盖代码区,使这些恶意代码能够执行。令人吃惊的是,非常多的软件使用不检查长度的调用(或曾经使用)。前一个脚注列出了部分常见的缓冲区bug,包括一些与安全相关的课题。

[后来的注释:时间的安排有时真是非常奇怪,所有旧玩意又变成了新东西。上面这些我写于八月中旬,正是红色代码II蠕虫大肆渲染于媒体之时。后来我才注意到微软对红色代码II所攻击的安全漏洞的描述:“由于idq.dll在一个处理输入URL的代码块中包含了一个不检查长度的缓冲区,所以导致了这个安全漏洞。攻击者可以在安装了idq.dll的服务器上创建一个web会话,进行溢出缓冲区的攻击,从而在web服务器上执行其代码。”我们应该有这方面的教训吧。(Here's the URL)]

7.“deprecated”这个词到底是什么意思,是在理论上还是在实际应用中?在标准中,“deprecate”表示一个特性,委员会警告你它可能在未来的某个时候消失,有可能就在下一个标准中。deprecate一个特性相当于“正式不提倡”,它是委员会反对你使用该特性但又不想立即把它从标准中拿掉所能采取的最强手段。实际上,即使是最deprecated的特性,要把它拿掉也是很困难的,因为它一旦在标准中消失,那些利用该特性编写代码的人们,以及每个标准团体都会厌恶这种向后不兼容。甚至在该特性被去除后,编译器厂商仍会继续提供该特性,因为他们也厌恶向后不兼容。经常,deprecated特性永远不会消失于标准中。例如,标准Fortran仍然保留了被deprecate了几十年的特性。

8.Stringstream提供一个接收string&参数的构造函数,但它只是简单地接收该string的一份拷贝,而不是直接把该string作为其工作区。

9.在表1的performance measurement(执行效率测试)部分,strstream在BC++5.5.1和VC7这两个平台上表现之糟糕令人大跌眼镜,原因大概是在这两个实现中,基于某些理由,每次调用例4的PrettyFormat( )时都要执行一些内存分配(尽管这两个实现在给定一个现存缓冲区进行工作时(象例4那样),比strstream不得不创建自己的缓冲区时内存分配次数还是要少一些)在其它的环境中,正如预料的那种,无需执行内存分配。

10.样本结果(Sample results)取3次测试的平均值,每次测试执行1,000,000次相应例子的代码,其结果可能因编译器版本以及开关设置的不同而有所差异。

原创粉丝点击