天天看點

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

繼續閱讀