1. 程式人生 > >Qt隱式共享與顯式共享

Qt隱式共享與顯式共享

log 引用 -- exp sdn resize 數據復制 turn name

版權聲明:本文為博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/Amnes1a/article/details/69945878
Qt中的很多C++類都使用了隱式數據共享來最大化資源使用和最小化拷貝代價。隱式共享類在作為參數傳遞時,不僅安全而且高效,因為只是指向數據的指針被傳遞了,底層的數據只有在函數向它執行寫動作時才會發生拷貝,即寫時拷貝。

一個共享類是由一個指向共享數據塊的的指針組成的,該數據塊包含一個引用計數和實際數據。

當一個隱式共享類的對象被創建時,它會引用計數設為1。無論何時,當一個新的對象引用這個共享數據時,引用計數會增加,當一個對象解引用這個共享數據時,引用計數會減少。當引用計數變為0時,共享數據會被刪除。

當處理共享數據時,通常有兩種方式拷貝一個對象。我們通常稱它們為深拷貝和淺拷貝。其中,深拷貝意味著復制一個對象;淺拷貝只拷貝引用,即只有指向共享數據的指針被復制。但執行一次深拷貝在內存和CPU方面的代價都是昂貴的。執行一次淺拷貝是非常快的,因為這只牽涉到設置一個指針和增加引用計數。隱式共享對象的賦值(使用operator=)被實現為淺拷貝。

共享的好處就在於程序不必執行不必要的拷貝,這就會促使更少的內存使用和更少的數據復制。這樣,對象就可以簡單的被賦值,作為函數的參數傳遞,作為函數的返回值。

隱式共享大多發生的背後,編程人員一般不需要關註它們。但是,隱式共享導致Qt的容器類和STL中的容器類有很大的不同。由於隱式共享,當復制一個容器時,它們其實是共享一份數據的。如下代碼所示:

QVector<int> a, b;
a.resize(100000); // make a big vector filled with 0.
QVector<int>::iterator i = a.begin();
b = a;
此處,叠代器i的使用要格外小心,因為它指向了共享數據。如果我們指向*i = 4,我們改變的將會是共享的實體,即會影響到兩個容器。
其實,在多線程應用程序中,隱式共享也會發生。從Qt4開始,隱式共享類就可以在線程間安全的拷貝,它們完全是可重入的。在很多人看來,考慮到引用計數的行為,隱式共享和多線程應該是不兼容的概念。但其實,Qt使用了原子引用計數來確保共享數據的完整性,避免了引用計數的潛在的錯誤。但原子引用計數並不能保證線程安全。當在線程間共享一個隱式共享類的實例時,還是應該使用合適的鎖。在對於所有的可重入類來說都是必須的,無論共享或不共享。但是,原子引用計數可以保證線程作用於它自己本地的一個隱式共享類的實例是安全的。我們通常建議使用信號和槽在線程間傳遞數據,因為這不需要任何顯式的鎖機制。

當然,除了Qt自帶的類的隱式共享,我們還可以使用QSharedData和QSharedDataPointer這兩個類來實現我們自己的隱式共享。

如果對象將要被改變並且其引用計數大於1,隱式共享會自動的從共享塊中分離該對象。(這經常被稱為寫時復制)

隱式共享類可以控制它自己的內部數據。在它的要修改數據的成員函數中,它會在修改數據之前自動的分離。但是,請註意,對於容器類的叠代器來說比較特殊,參見上面所講。

下面,我們以一個員工類為例,來實現一個隱式共享類。步驟如下:

定義類Emplyee,該類只有一個唯一的數據成員,類型為QSharedDataPointer<EmployeeData>。
定義類EmployeeData類,其派生自QSharedData。該類中包含的就是原本應該放在Employee類中的那些數據成員。
類定義如下:
#include <QSharedData>
#include <QString>

class EmployeeData : public QSharedData
{
public:
EmployeeData() : id(-1) { }
EmployeeData(const EmployeeData &other)
: QSharedData(other), id(other.id), name(other.name) { }
~EmployeeData() { }

int id;
QString name;
};

class Employee
{
public:
Employee() { d = new EmployeeData; }
Employee(int id, const QString &name) {
d = new EmployeeData;
setId(id);
setName(name);
}
Employee(const Employee &other)
: d (other.d)
{
}
void setId(int id) { d->id = id; }
void setName(const QString &name) { d->name = name; }

int id() const { return d->id; }
QString name() const { return d->name; }

private:
QSharedDataPointer<EmployeeData> d;
};
在Employee類中,要註意這個數據成員d。所有對employee數據的訪問都必須經過d指針的operator->()來操作。對於寫訪問,operator->()會自動的調用detach(),來創建一個共享數據對象的拷貝,如果該共享數據對象的引用計數大於1的話。也可以確保向一個Employee對象執行寫入操作不會影響到其他的共享同一個EmployeeData對象的Employee對象。
類EmployeeData繼承自QSharedData,它提供了幕後的引用計數。
在幕後,無論何時一個Employee對象被拷貝、賦值或作為參數傳,QSharedDataPointer會自動增加引用計數;無論何時一個Employee對象被刪除或超出作用域,QSharedDataPointer會自動遞減引用計數。當引用計數為0時,共享的EmployeeData對象會被自動刪除。

void setId(int id) { d->id = id; }

void setName(const QString &name) { d->name = name; }
在Employee類的非const成員函數中,無論何時d指針被解引用,QSharedDataPointer都會自動的調用detach()函數來確保該函數作用於一個數據拷貝上。並且,在一個成員函數中,如果對d指針進行了多次解引用,而導致調用了多次detach(),也只會在第一次調用時創建一份拷貝。
int id() const { return d->id; }

QString name() const { return d->name; }
但在Employee的const成員函數中,對d指針的解引用不會導致detach()的調用。

還有,沒必要為Employee類實現拷貝構造函數或賦值運算符,因為C++編譯器提供的拷貝構造函數和賦值運算符的逐成員拷貝就足夠了。因為,我們唯一需要拷貝的就是d指針,而該指針是一個QSharedDataPointer,它的operator=()僅僅是遞增了共享對象EmployeeData的引用計數。

隱式共享 VS 顯式共享
上面講到的隱式共享,對於Employee類來說可能會有問題。考慮一種情況,如下代碼所示:
#include "employee.h"

int main()
{
Employee e1(1001, "Tom");
Employee e2 = e1;
e1.setName("Jerry");
}
在創建e2,並將e1賦值給它後,e1和d2都引用了同一個員工,即Tom,1001。這兩個Employee對象指向了同一個EmployeeData實例,所以該實例的引用計數為2。緊接著執行e2.setName("Jerry")來改變員工名字,但因為此時的引用計數大於1,所以會在名字發生改變前執行一次寫時復制,使e1和e2指向不同的EmployeeData對象,然後對e1執行名字修改動作。從而導致e1和e2有不同的名字,但有相同的ID,1001,這可能不是我們想要的。當然,如果我們確實想創建出第二個完全不同的員工,那麽可以再在e1上調用setId(1002),修改其ID。但是,如果我們就是只想改變員工的名字呢?此時,我們就可以考慮使用顯式共享來替代隱式共享。
如果我們在Employee類中的聲明d指針時使用的是QExplicitySharedDataPointer<EmployeeData>,那麽就是使用了顯式共享,寫時復制操作就不會自動發生了(即 在 非 const成員函數中不會自動調用detach())。這樣一來,e1.setName("Jerry")執行之後,員工的名字被改變了,但e1和e2仍然引用同一個EmployeeData實例,故還是只有一個id為1001的員工。

---------------------
作者:求道玉
來源:CSDN
原文:https://blog.csdn.net/Amnes1a/article/details/69945878
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!

Qt隱式共享與顯式共享