第15条 注意string实现的多样性
实现string的方式很多。
几乎每个string实现都包含如下信息:
1)字符串的大小(size),即它所包含的字符的个数。
2)用于存储该字符串中字符的内存的容量(capacity)。
3)字符串的值(value),即构成该字符串的字符。
除此之外,一个string还可能包含:
4)它的分配子的一份拷贝。这个字段是可选的。
建立在引用计数基础上的string实现可能还包含:
5)对值得引用计数。
书中写了4种string的实现,结构依次如下:
实现A:包含其分配子的一份拷贝 该字符串的大小 它的容量,以及一个指针,该指针指向一块动态分配的内存,其中包含了引用计数(refCnt)和字符串的值。
在该实现中,使用默认分配子的string对象大小是一个指针的4倍。若使用了自定义的分配子,则string对象会更大一些,多出的部分取决于分配子对象的大小。
实现B: string对象与指针大小相同,因为它只包含一个指向结构的指针。这里嘉定使用了默认的分配子。如果使用了自定义的分配子,则string对象的大小将会相应的加上分配子对象的大小。在该实现中,由于用到了优化,所以使用默认的分配子不需要多余的空间。而实现A中没有这种优化。
B的string所指向的对象中包含了该字符串的大小,容量和引用计数,以及一个指向一块动态分配的内存的指针,该内存中存放了字符串的值。该对象还包含了一些与多线程环境下的同步控制相关的额外数据。这些数据暂不讨论,标记为“其他”。
标记为“其他”的数据块比其他的要大。用于实现同步控制的数据是指针的大小的6倍。
实现C:string对象的大小总是与指针的相同,而该指针所指向一块动态分配内存,其中包含了与该字符串相关的一切数据:它的大小,容量,引用计数和值。没有用对单个对象的分配子支持。该内存中也包含了一些与值的可共享性相关的数据。这里不考虑,标记为“X”。
实现D:string对象是指针的大小的7倍(仍然假定使用的是默认的分配子)。这一实现不使用引用计数,但是每个string内包含一块内存,最大可容纳15个字符的字符串,小字符串可以玩转的存放在该string对象中,这一特性成为”小字符串优化“特性。当一个string的容量超过15时,该内存的起始部分被当做一个指向一块动态分配的内存的指针,而该string的值就放在这块内存中。
例如一个strings(“pause”);
在实现D中不会导致任何动态分配,在A和C中将导致一次动态分配,而在B中江导致两次动态分配(1次为string对象所指向的对象,另一次是为该对象所指向的字符缓冲区),但是B包含了对多线程系统中同步的支持,所以自己要选用哪种实现,需要结合自己的需要,选择不同的实现版本。
在以引用计数为基础的设计方案中,string对象之外的一切都可以被多个string共享,所以我们还可以得出,A比B或C提供了较小的共享能力,B和C可以共享string的大小和容量,从而见笑了每个对象存储这些数据的评价开销。D中所以的string不共享任何数据。
不同实现的区别有:
1) string的值可能会被引用计数,也可能不会。很多实现在默认情况下回使用引用计数,但它们通常提供了关闭默认选择的方法,往往是通过预处理宏来做到这一点的。第13条给出了想将其关闭的一种特殊情况,但其他的原因也有可能。比如,只有当字符串被频繁复制时,引用计数才有用,而有些应用并不经常复制内存,这就不值得使用引用计数了。
2) string对象大小的范围可以是一个char*指针的大小的1到7倍。
3) 创建一个新的字符串值可能需要0次,一次或两次动态分配内存。
4) String对象可能共享,也可能不共享其大小和容量信息。
5) String可能支持,也可呢过不支持对单个对象的分配子(C不支持)
6) 不同的实现对字符内存的最小分配单位有不同的策略。