天天看点

Qt隐式共享detach函数的理解

隐式数据共享

Qt隐式共享的详细描述,请在Qt Assistant 索引查阅,如下:

Qt隐式共享detach函数的理解

以下是本人的理解:

Qt 中的许多 C++ 类使用隐式数据共享来最大化资源使用并最小化复制。当作为参数传递时,因为只传递指向数据的指针,并且只有在函数写入时才复制数据(即写时复制),故隐式共享类既安全又高效,

共享类由指向共享数据块的指针组成,该指针包含引用计数和数据。

创建共享对象时,它将引用计数设置为 1。每当新对象引用共享数据时,引用计数就会增加,当对象取消引用共享数据时,引用计数会减少。当引用计数变为零时,共享数据将被删除,类似C++中的共享智能指针。

在处理共享对象时,有两种复制对象的方法:深拷贝和浅拷贝。

  • 深拷贝意味着复制一个对象。
  • 浅拷贝是引用拷贝,即只拷贝指向共享数据块的指针,而不是拷贝指针指向的对象。

就内存和 CPU 而言,进行深度复制可能会很昂贵。进行浅拷贝非常快,因为它只涉及设置指针和增加引用计数。

隐式共享对象的对象分配(使用 operator=())是使用浅拷贝实现的。

共享的好处是程序不需要不必要地复制数据,从而减少内存使用和数据复制。对象可以很容易地被赋值,作为函数参数发送,并从函数中返回。

如下A为对象,且当前只有一个A类型的指针指向该对象,暂且称为pA1,类型为A*,

Qt隐式共享detach函数的理解

此时A*的类内部的引用计数refCount 为1。当有N个A类型的指针指向A对象时,即如下图所示:

Qt隐式共享detach函数的理解

此时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()函数内部实现机制是:

  1. 先深度拷贝一个A对象,拿上面的代码来说就是深度复制出一个QPen。
  2. 更新操作只在步骤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,证明其它地方也在调用该对象,那么就深度拷贝一个该对象
      }
  }
           

即按下图进行:

Qt隐式共享detach函数的理解

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的对象被更改。

继续阅读