隐式数据共享
Qt隐式共享的详细描述,请在Qt Assistant 索引查阅,如下:
以下是本人的理解:
Qt 中的许多 C++ 类使用隐式数据共享来最大化资源使用并最小化复制。当作为参数传递时,因为只传递指向数据的指针,并且只有在函数写入时才复制数据(即写时复制),故隐式共享类既安全又高效,
共享类由指向共享数据块的指针组成,该指针包含引用计数和数据。
创建共享对象时,它将引用计数设置为 1。每当新对象引用共享数据时,引用计数就会增加,当对象取消引用共享数据时,引用计数会减少。当引用计数变为零时,共享数据将被删除,类似C++中的共享智能指针。
在处理共享对象时,有两种复制对象的方法:深拷贝和浅拷贝。
- 深拷贝意味着复制一个对象。
- 浅拷贝是引用拷贝,即只拷贝指向共享数据块的指针,而不是拷贝指针指向的对象。
就内存和 CPU 而言,进行深度复制可能会很昂贵。进行浅拷贝非常快,因为它只涉及设置指针和增加引用计数。
隐式共享对象的对象分配(使用 operator=())是使用浅拷贝实现的。
共享的好处是程序不需要不必要地复制数据,从而减少内存使用和数据复制。对象可以很容易地被赋值,作为函数参数发送,并从函数中返回。
如下A为对象,且当前只有一个A类型的指针指向该对象,暂且称为pA1,类型为A*,
此时A*的类内部的引用计数refCount 为1。当有N个A类型的指针指向A对象时,即如下图所示:
此时A*的类内部的引用计数refCount 为n,可以看到这些指针都是共享A,并没有单独复制一份A对象,即没有深度复制A对象,也就是所谓的浅拷贝。当需要对某个指针指向的A对象的属性进行更新、修改时,其它指针的调用方此时发现其所指向的A也跟着改变了(因为它们都是指向同一个A对象),这在很多情况下,不是调用方所希望的结果。比如:A是QPen对象,如下代码:
void QPen::setStyle(Qt::PenStyle style)
{
d->style = style; // set the style member
}
......................// 其它代码
QPen* pPen1 = new QPen; // QPen的引用计数为1
QPen* pPen2 = pPen1; // QPen的引用计数为2
// 刚new出来的QPen对象被pPen2更改了,此时pPen1的调用方也跟着变化了,此时pPen1的调用方
// 功能就会不正确,就会感觉诡异,心里暗想:我明明啥都没改,怎么画笔就变了呢?
pPen2->setStyle(Qt::CustomDashLine);
刚new出来的QPen对象被pPen2更改了,此时pPen1的调用方也跟着变化了,此时pPen1的调用方
功能就会不正确,就会感觉诡异,心里暗想:我明明啥都没改,怎么画笔就变了呢?detach正是为了解决这个问题而提出!!!对于上面情况,detach()函数内部实现机制是:
- 先深度拷贝一个A对象,拿上面的代码来说就是深度复制出一个QPen。
- 更新操作只在步骤1深度拷贝出的对象上进行。
即:
void QPen::setStyle(Qt::PenStyle style)
{
detach(); // detach from common data
// 这里的d是detach()函数深度拷贝出来的新对象,而不再是原来的那个。
d->style = style; // set the style member
}
void QPen::detach()
{
if (d->ref != 1) {
... // 发现引用计数大于1,证明其它地方也在调用该对象,那么就深度拷贝一个该对象
}
}
即按下图进行:
pA2即为要更新的对象。先将pA2指向A对象断开,再深度复制一个A,让 pA2指向新复制出来的这个A对象。此时对新复制出来的A无论怎么修改都不会影响到原来的A对象,即非pA2的指针的调用方不会受影响。
对于QSharedDataPointer类型(QSharedDataPointer用法请参考《QPointer、QScopedPointer、QSharedDataPointer等指针用法总结》)的clone函数,当引用计数大于 1 时,clone函数由 detach() 调用以创建新副本。此函数使用运算符 new 并调用类型 T 的拷贝构造函数。QSharedDataPointer类型的的T *data() / T *get(),返回指向共享数据对象的指针。这两个函数也调用 detach()。QSharedDataPointer类型设计clone、data() 、 get()函数的目的就是为了对指向的对象进行更改,所以必须是深度复制才行。QSharedDataPointer类型的const T *constData() 、const T *data() / const T *get() 设计目的从const就可以看出仅仅是读取指向的对象,而不是进行写入更改操作,故带const的这三个函数不会调用detach,因为浅拷贝就可以完成,用浅拷贝进行读取共享对象不会导致被读取的共享对象更改,所以没必要进行资源高、效率低的深拷贝。
总结:detach发生两个必要条件是:
- 隐式共享的对象个数超过1个,即引用计数大于1.
- 即将要detach的对象被更改。