得注意进度了,稍微写快一点得
- 条款15:小心string实现的多样性
1.一个小问题,sizeof(string)的大小是多少,如果我们很注意内存消耗的话,这可能是个十分重要的问题或是你希望用string来代替一个char*.关于这个问题的结果,如果我们担心空间问题,这个问题的答案几乎都是我们不希望听到的.string和char*一样大的实现很常见,也很容易找到string是char*指针7倍大小的实现.(我在我的电脑上测试 g++ 5.4.0,string大小是32字节,char*大小是8字节).-
让我们来看看为什么会有这些不同的差异.首先我们来看看string可能要存什么数据和它可能决定保存在哪里.实际上每个string实现都容纳了以下的信息:
- 字符串的大小,也就是它包含的字符的数目.
- 容纳字符串字符的内存容量.(注意内存容量和字符串大小的区别,在前面条款14中已经说过).
- 这个字符串的值,也就是字符串的内容.
另外,一个字符串可能容纳
- 它的配置器的的拷贝,这个在条款10中有介绍.
依赖引用计数的string实现也包含了
- 这个值的引用计数
-
下面我们来看看具体的几个实现,在实现A中,每个string对象包含一个它配置器的拷贝,字符串的大小,它的容量,和一个指向包含引用计数和字符串值的动态分配的缓冲区的指针.在这个实现中,一个使用默认配置器的字符串对象是指针大小的四倍.对于一个自定义的配置器,string对象会随着配置器对象的增大而增大:
image.png -
实现B的string对象和指针一样大.因为在结构体中只包含一个指针.再次,这里假设使用默认配置器,否则string对象肯定会因为自定义配置器对象的增大而增加.在这个实现中,使用默认配置器不占用空间,这归功于这里用了一个在实现A中没有的使用优化.B的string指向的对象包含字符串的大小,容量和引用计数,以及容纳字符串值的动态分配缓冲区的指针.对象也包含在多线程系统中与并发控制有关的一些附加数据.这些数据在我们这里不列入考虑,标记为其他.
image.png
值得注意的是,这个"other"的框比其他框要大,因为这个图是按比例画出来的.在实现B中,用于并发控制的数据是一个指针大小的6倍.
-
实现C的string对象总是等于指针的大,但是这个指针指向一个包含所有与string相关的东西的动态分配缓冲器:它的大小,容量,引用计数和值.没有每物体配置器的支持(就是自定义每个对象的内存配置器).缓冲区也容纳一些关于值可共享性的数据,这个主题在more effective c++条款29中有叙述,这里我们不考虑,所以标记为X.
image.png -
实现D的string对象是一个指针大小的7倍(仍然假设使用了默认配置器).这个实现没有引用计数,但每个string包含了一个足以表现最多15个字符的字符串值的内部缓冲区.因此,小的字符串可以被保存在string对象中,一个有时候被称为"小字符串优化"的特性.当一个string的容量超过15时,缓冲器的第一部分被用作指向动态分配内存的一个指针,而字符串的值放在那块内存中.
image.png -
现在我们来看看一个具体的例子:
string s("Perse);
这句话在实现D下将会没有动态分配,在实现A和C下一次,在实现B下是两次(一次是string对象指向的对象,一次是那个对象指向的实际字符缓冲区).如果我们关心动态分配和回收内存的次数,或如果我们关心经常伴随这样分配的内存开销,我们可能需要避开实现B.另一个方面来看,实现B的数据结构包括了对多线程系统并发控制的特殊支持的事实意味着它比实现A和实现C更多的满足你在多线程环境下的要求,尽管动态分配的次数比较多.(实现D则不需要对多线程的特殊支持,因为它不使用引用计数.条款12,13介绍了更多线程和引用计数字符串之间的关系.)
-
在基于引用计数的设计中,字符串对象之外的每个东西都可以被多个字符串共享(如果它们有相同的值),所以我们可以从图中观察到的其他东西是实现A比B或C提供更少的共享性(这是很显然的,因为A的指针只指向了string的值,没有额外的东西,所以只能共享值,而不能共享其他额外的东西).特别是,实现B和C都能共享一个字符串的大小和容量,因此潜在的减少了每个物体分摊的储存数据的开销.不过,有意思的是,实现C不能支持每对象适配器的事实意味着它是唯一可以共享配置器的实现,所有字符串必须使用同一个.实现D在字符串对象间没有共享数据.
-
需要注意的是,我们不能完全从图中推断出关于小字符串的内存管理策略.有些实现拒绝为小于一个适当字符数分配内存,实现A,C,D就是这样.再看一句代码:
string s("Perse");
实现A有32个字符的最小分配大小.所以,虽然在所有实现下s的大小是5,但是在实现A中它的容量是31.(第32个字符可能是被保留作为尾部的NULL,因此可以方便的实现c_str成员函数.)实现C也有一个最小量,但它是16,而且没有为尾部NULL留下空间.故在实现C下,s的容量是16.在这里,实现D对于大小小于16的字符串是不会动态分配内存的,字符串使用的内存包含在本身字符串对象中.实现B没有最小分配的要求,在实现B下,s的容量是7(这个问题作者也没有细细去看源码,估计是设计上的一些做法).
-
让我们来总结一下来看看,string的多种实现:
- 字符串值可能是或者不是引用计数的.在默认情况下,很多实现都使用了引用计数,但是它们通常也提供了关闭的方法,一般是通过预处理宏.条款13有介绍可能要关闭引用计数的特殊环境的例子.但在一些特殊情况下,我们可能也会需要关闭,因为程序可能不经常拷贝字符串,所以不需要这个开销.
- string对象的大小可能从1倍char*指针到7倍char*指针的大小.
- 新字符串值的建立可能需要0,1或2次动态分配.
- string对象可能是或可能不共享字符串的大小和容量信息.
- string可能是或可能不支持每对象配置器.(就是自定义每个对象的内存配置器)
- 不同实现对于最小化字符缓冲区的配置器有不同策略
string是STL中最重要的组件之一,我们应该尽可能经常的使用它,但是我们在使用的过程中一定要考虑到不同的string的实现可能对我们代码造成的影响,特别是如果我们在写必须在不同STL平台上运行的代码并且你面临严格的性能需求.
-
网友评论