QString内部有共享机制。所谓共享机制,就是假若将QString类型的字符串str1赋值给str2,实际str2并没有拷贝str1的数据,只是将str2中一个指向实际数据的指针指向了str1。意味着str1和str2的存储数据的内存是同一块地址。这样就实现了共享,节省内存。
但是,这样带来了问题:假如后面改了str2的数据,那个str1的数据也会跟着改变,这并不是用户想要的结果。
Qt在此问题上采用写时复制机制。也就是,在修改str2的数据时,先申请一块内存,将str1和str2共享的数据拷贝到新的内存中,然后将str2指向新的内存地址,这样str1和str2就不再共享一块内存,修改str2的数据不会影响str1。这就是QString的写时复制。
写时复制(copy-on-write):需要写数据的时候,才进行复制。
一、原理分析
1、QStringData类
QStringData类是Qt的一个内部类,不对外使用。在QString的内部,使用QStringData存储数据。QStringData类是一个模板类QTypeArrayData,QTypeArrayData是一个Array模板,也就会一个数组,里面每一个元素就是一个字符,因此每一个字符都是ushort型,大小为2字节,存放的是字符对应的unicode码。
qstringliteral.h
在QString中,将QStringData定义为Data。
qstring.h
2、QTypeArrayData和QArrayData类
qarraydata.h
QTypeArrayData是继承自QArrayData,在QArrayData的基础上,添加了一些方法(迭代器)方便访问和修改元素,而QArrayData是一个数据容器,内部实现较为纯粹,仅提供数据的逻辑,因此内存分配和回收等业务逻辑还是在QArrayData类里。而 QTypeArrayData的内存相关的方法,都是转调QArrayData的方法。
QArrayData类有几个重要的函数涉及到内存。如 allocate,deallocate等。
其中的allocate函数的实现逻辑:先计算分配的字节数allocSize,字节数=header + 字符数*2。本例中字符数为6,header大小固定为16 ,计算后一共需要分配28字节。然后调用C库函数malloc,从堆上分配内存。
qarraydata.cpp
上面说的header大小固定为16,下面对其分析:
header的类型为QArrayData,有5个成员变量,ref,size,alloc,capacityReserved,offset。
其中alloc和capacityReserved共同占32bit,也就是4字节,size为int类型,占4字节,offset是一个指针类型,也占4字节。ref的类型RefCount类型,RefCount类型定义如下:
只有一个QBasicAtomicInt 类型的成员变量,而QBasicAtomicInt是一个模板类QBasicAtomicInteger
QBasicAtomicInteger有一个T类型的成员变量,因此sizeof(QBasicAtomicInteger) = sizeof(T),
那么sizeof = sizeof<QBasicAtomicInteger> = sizeof(int) = 4
经过上面的分析,header =4+4+4+4 =16 字节。
3、QString类
经上面的分析得知QString内部的数据存储是由QStringData(也即Data)完成的
QString的构造函数之一,调用QStringData的alloc分配内存。d是QString的一个成员变量,类型就是QStringData,因此d指向了分配好的内存。
qstring.h
(1)、QString的构造和析构
QString的析构函数,并不是直接将d析构掉,而是有条件的,而是当d->ref.deref()为0时,才进行析构。d->ref.deref()是引用计数,因此QString的写时复制是由引用计数机制实现的。
E:\Code\vcpkg\installed\x86-windows\include\QtCore\qarraydata.h
e:\Code\vcpkg\buildtrees\qt5-base\src\5.12.8-345bb910e3\src\corelib\tools\qrefcount.h
(2)、拷贝构造和赋值运算符
将一个QString赋值给另一个QString,为啥没有发生拷贝呢?通过源码分析:
qstring.h
可以看到,新对象的d直接用老对象的,并没有进行内存的分配和拷贝,只是简单的将引用计数增加了1。
(3)、修改字符
可以看到,先返回s.detach(),s.detach()是什么呢?
detach()就是重写分配内存后的地址空间。
至于isShared是判断是否有其他的对象引用了此处内存。