
这篇文章建议衔接上一篇内容来看体验更佳C++ string类模拟实现指南:构造、遍历、修改与常用接口
库中string类的底层还有一些小问题

s2后给的字符串不是存到string对象本身的空间上面的,而是存在该对象指向的堆空间上,所以这里s1对象和s2对象的大小是没有任何区别的。根据其成员变量,理论上在 32 位系统中,char* 和size_t占 4 字节,总大小为 4 + 4 + 4 = 12字节,在 64 位系统中,char* 占 8 字节,size_t占 8 字节,因此总大小为 8(_str) + 8(_size) + 8(_capacity) = 24字节。 然而实际在32位系统下,这里的大小是28字节
这里看一下底层结构

这里字符个数小于等于15的时候就会存到_Buf当中,大于15的时候,_Buf就丢弃不用了,而是存到_Ptr指向的空间当中

这是一种通用的内存管理设计技术,叫做小对象优化(SBO),在大量的开辟小块空间的时候,不仅会有内存碎片的问题,而且效率也是不够高,SBO本质上是一种以空间换时间的处理方式,当对象比较小的时候就会在对象中存一个_Buf(在对象内部预留固定大小的缓冲区直接存储小对象数据,_Buf是一个小数组,在对象开数组是比堆上开数组快的)而不去堆上开空间(减少动态内存分配开销和碎片化问题),这种处理方式通常用于在堆上开空间的结构
当对象比较大的时候就会再用另外一个数组_Ptr,而不会两个数组各存一部分,若各存一部分像用c_str都无法返回给值,这样就浪费了一个数组的空间了

所以整个代码访问数据的位置就要判断数据到底放在哪里,字符串_size小于16放_buff当中,大于等于16放_str当中。此时对其再算,在32位平台下整个对象空间就是28字节了
VS一贯采用该技术来进行内存管理优化,Linux系统下的早期的G++是采用另外一种方案(COW COPY - ON - WRITE):引用计数,其库中string设计的时候默认的拷贝是进行浅拷贝

两个对象指向同一块空间,该空间的引用计数为2,之后进行析构(例如先析构s2),编译器就会先看引用计数,发现引用计数为2,说明还有其他对象一同管理这块空间,此时就会减减计数(1),之后s1进行析构,再减减计数(0),此时计数为0就说明s1就是最后一个管理这块空间的对象,此时再释放空间

到这里只解决了析构多次的问题,浅拷贝还有另外一种问题,两个对象指向同一块空间时,一个对象的修改会影响另一个对象,这里就还有一种操作叫做写时拷贝,该操作也是依托于引用计数,若进行写的时候(例如:s1[0]=‘x’)就会去检查引用计数,若引用计数为1才会进行写的操作,此时只有一个对象指向这块空间,若引用计数不是1还要开一块空间拷贝数据,之后原空间计数减减,新空间也会有计数(为1),此时再进行s1[0]='x’的操作就没有问题了

G++早年用的该方案也叫引用计数的写时拷贝
G++这样设计饶了一大圈也是存在优势的,因为拷贝了对象是不一定会修改对象的,以下面代码为例
若早年的编译器没有优化,下面就会进行多次深拷贝,addret是个局部对象,不能引用返回。这里addret作返回的时候会先把其拷贝给一个临时对象,再将临时对象拷贝给ret。这里先用VS的方案(直接进行深拷贝),addret指向一块空间,addret拷贝给临时对象时候,中间会进行拷贝构造,就会给临时对象开和addret一样大小的空间,数据拷贝过来后,该临时对象作为函数调用表达式的返回值,addret就析构了,然后再将临时对象拷贝给ret,ret也和临时对象有一样大的空间一样的值了,此时临时对象也销毁

这中场景采用引用计数的写时拷贝方案就完胜了,刚开始addret指向的这块空间引用计数为1,浅拷贝后临时对象也指向该空间,引用计数变为2,之后addret销毁,引用计数减减变为1,临时对象浅拷贝给ret,ret也指向临时对象指向的空间,引用计数变为2,临时对象再销毁,引用计数减减(变为1),使用该方案就没有拷贝了
这里最终就是多花一点成本来维护引用计数,所需的前提条件就是拷贝后没有修改,就能达成提升效率的目的,所以说该技术的设计是非常厉害的
但是该方案也是有很多问题的:
所以后面的GCC版本就不采用该方案了