effective STL - 小心string实现的多样性

 

 

Bjarne Stroustrup曾经用奇特的标题写一篇文章,《Sixteen Ways to Stack a Cat》[2。事实表明实现string几乎有和那一样多的方法。当然,作为有经验而且老于世故的软件工程师,我们应该忽视“实现细节”,但是如果爱因斯坦是对的,上帝存在于细节里,现实要求我们有时皈依宗教。即使当细节不重要的时候,对它们有一些了解使我们能够确信它们不重要

例如,一个string对象的大小是多少?换句话说,sizeof(string)返回什么值?如果你正密切注意内存消耗,这可能是一个重要的问题,或你正想用一个string对象代替一个原始的char*指针。

 

关于sizeof(string)的消息是“有趣”,如果你担心空间问题,这几乎肯定是你不想听到的。string和char*指针一样大的实现很常见,也很容易找到string是char*7倍大小的string实现。为什么会有差别?为了理解这一点,我们必须知道string可能存什么数据和它可能决定保存在哪里。

实际上每个string实现都容纳了下面的信息:

  • 字符串的大小,也就是它包含的字符的数目.一个size_t.
  • 可以容纳字符串字符的内存容量。一个size_t. 
  • 这个字符串的,也就是,构成这个字符串的字符。一个char*

另外,一个string可能容纳

  • 它的配置器的拷贝。

依赖引用计数的string实现也包含了

  • 这个值的引用计数

不同的string实现以不同的方式把这些信息放在一起。为了证明我的意思,我将让你看四种不同的string实现使用的数据结构。并不是要特别选择这些实现,它们都来自于常用的STL实现,而正好是我检查的前四个库的string实现。

在实现A中,每个string对象包含一个它配置器的拷贝,字符串的大小,它的容量,和一个指向包含引用计数(“RefCnt”)和字符串值的动态分配的缓冲区的指针。在这实现中,一个使用默认配置器的字符串对象是指针大小的四倍。对于一个自定义的配置器,string对象会随配置器对象的增大而变大:

图15-1

实现B的string对象和指针一样大,因为在结构体中只包含一个指针。再次,这里假设使用默认配置器。正如实现A,如果使用自定义配置器,这个string对象的大小会增加大约配置器对象的大小。在这个实现中,使用默认配置器不占用空间,这归功于这里用了一个在实现A中没有的使用优化。

 

B的string指向的对象包含字符串的大小、容量和引用计数,以及容纳字符串值的动态分配缓冲区的指针。对象也包含在多线程系统中与并发控制有关的一些附加数据。这样数据在我们考虑之外,所以我只是把数据结构的那部分标记为“其他”:

图15-2

“其他”的框比其它框大,因为我按比例画框。如果一个框大小是另一个的两倍,大的框使用的字节数是小的两倍,在实现B中,用于并发控制的数据是一个指针大小的6倍。

 

实现C的string对象总是等于指针的大小,但是这个指针指向一个包含所有与string相关的东西的动态分配缓冲器:它的大小、容量、引用计数和值。没有每物体配置器(per-object allocator)的支持。缓冲区也容纳一些关于值可共享性的数据,我们在这里不考虑这个主题,所以我标记为“X”。(如果你首先对为什么一个引用计数值可能不可共享感兴趣,参考《More Effective C++》的条款29。)

图15-3

实现D的string对象是一个指针大小的七倍(仍然假设使用了默认配置器)。这个实现没有使用引用计数,但每个string包含了一个足以表现最多15个字符的字符串值的内部缓冲区。因此小的字符串可以被整个保存在string对象中,一个有时被称为“小字符串优化”的特性。当一个string的容量超过15时,缓冲器的第一部分被用作指向动态分配内存的一个指针,而字符串的值存放在那块内存中:

图15-4

这些图不仅证明了我能读源代码并能画漂亮的照片,而且它们也让你可以推断出在这样的语句中建立string,

string s("Perse");		// 我们的狗叫做“Persephone”,但我们
			// 一般只叫她“Perse”。访问她的网站
			// http://www.aristeia.com/Persephone/

在实现D下将会没有动态分配,在实现A和C下一次,而在实现B下两次(一次是string对象指向的对象,一次是那个对象指向的字符缓冲区)。如果你关心动态分配和回收内存的次数,或如果你关心经常伴随这样分配的内存开销,你可能想要避开实现B。另一方面, 实现B的数据结构包括了对多线程系统并发控制的特殊支持的事实意味着它比实现A或C更能满足你的需要,尽管动态分配次数较多。(实现D不需要对多线程的特殊支持,因为它不使用引用计数。条款13讲了更多线程和引用计数字符串之间的关系。更多关于你可能希望的STL容器中的线程支持方面的信息,参考条款12。)

在基于引用计数的设计中,字符串对象之外的每个东西都可以被多个字符串共享(如果它们有相同的值),所以我们可以从图中观察到的其他东西是实现A比B或C提供更少的共享性。特别是,实现B和C能共享一个字符串的大小和容量,因此潜在地减少了每物体分摊的的储存数据的开销。有趣的是,实现C不能支持每对象配置器的事实意味着它是唯一可以共享配置器的实现:所有字符串必须使用同一个!(再次,管理分配器规则的细节在条款10。)实现D在字符串对象间没有共享数据。

你不能完全从图中推断出的字符串行为的一个有趣方面是关于小字符串的内存管理策略。有些实现拒绝为小于一个适当字符数分配内存,实现A、C和D就是这样。再看看这条语句:

string s("Perse");		// s是一个大小为5的字符串

实现A有32个字符的最小分配大小,所以虽然在所有实现下s的大小是5,在实现A下它的容量是31。(第32个字符大概被保留作尾部的null,因此可以容易地实现c_str成员函数。)实现C也有一个最小量,但它是16,而且没有为尾部null保留空间。所以在实现C下,s的容量是16。实现D的最小缓冲区大小也是16,包括尾部null的空间。当然,在这里区别出实现D是因为容量小于16的字符串使用的内存包含在本身字符串对象中。实现B没有最小分配,在实现B下,s的容量是7。(为什么不是6或5。我不知道。我没有那么细致地读那些源代码,抱歉。)

如果你预计会有许多短字符串和两者中任何一个(1)你的释放环境内存非常小或(2)你关心引用的地点而且想要把字符串聚集在尽量少的页面中,我觉得对于最小分配的各种各样的实现策略可能对你很重要。

很显然,string实现的自由度比乍看之下多得多,也很显然,不同的实现以不同的方式从它们的设计灵活性中得到好处。让我们总结一下:

  • 字符串值可能是或可能不是引用计数的。默认情况下,很多实现的确是用了引用计数,但它们通常提供了关闭的方法,一般是通过预处理器宏。条款13给了一个你可能要关闭的特殊环境的例子,但你也可能因为其他原因而要那么做。比如,引用计数只对频繁拷贝的字符串有帮助,而有些程序不经常拷贝字符串,所以没有那个开销。
  • string对象的大小可能从1到至少7倍char*指针的大小。
  • 新字符串值的建立可能需要0、1或2次动态分配。
  • string对象可能是或可能不共享字符串的大小和容量信息。
  • string可能是或可能不支持每对象配置器。
  • 不同实现对于最小化字符缓冲区的配置器有不同策略。

现在,不要误解我。我认为string是标准库中的最重要的组件之一,而且我鼓励你尽可能经常地使用它

而且,string好像很简单。谁会想到它的实现可以如此有趣?

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值