Window系统下开发Qt(8)-- QString 写时拷贝

171 阅读2分钟

 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是判断是否有其他的对象引用了此处内存。