1.淺拷貝:
淺拷貝就比如像引用類型
淺拷貝是指源對象與拷貝對象共用一份實體,僅僅是引用的變量不同(名稱不同)。對其中任何一個對象的改動都會影響另外一個對象。舉個例子,一個人一開始叫張三,后來改名叫李四了,可是還是同一個人,不管是張三缺胳膊少腿還是李四缺胳膊少腿,都是這個人倒霉。
2.深拷貝:
而深拷貝就比如值類型。
深拷貝是指源對象與拷貝對象互相獨立,其中任何一個對象的改動都不會對另外一個對象造成影響。舉個例子,一個人名叫張三,后來用他克隆(假設法律允許)了另外一個人,叫李四,不管是張三缺胳膊少腿還是李四缺胳膊少腿都不會影響另外一個人。比較典型的就是Value(值)對象,如預定義類型Int32,Double,以及結構(struct),枚舉(Enum)等。
3.隱式共享:
隱式共享又叫做回寫復制。當兩個對象共享同一份數據時(通過淺拷貝實現數據塊的共享),如果數據不改變,不進行數據的復制。而當某個對象需要改變數據時則執行深拷貝。
QString類采用隱式共享技術,將深拷貝和淺拷貝有機地結合起來。
例如:
void MainWindow::on_pushButton_8_clicked() { QString str1="data"; qDebug() << " String addr = " << &str1 <<", "<< str1.constData(); QString str2=str1; //淺拷貝指向同一個數據塊 qDebug() << " String addr = " << &str2 <<", "<< str2.constData(); str2[3]='e'; //一次深拷貝,str2對象指向一個新的、不同於str1所指向的數據結構 qDebug() << " String addr = " << &str2 <<", "<< str2.constData(); str2[0]='f'; //不會引起任何形式的拷貝,因為str2指向的數據結構沒有被共享 qDebug() << " String addr = " << &str2 <<", "<< str2.constData(); str1=str2; //str1指向的數據結構將會從內存釋放掉,str1對象指向str2所指向的數據結構 qDebug() << " String addr = " << &str1 <<", "<< str1.constData(); qDebug() << " String addr = " << &str2 <<", "<< str2.constData(); }
實測輸出結果如下(括號內是我的分析):
String addr = 0x28c79c , 0x14316660 (str1的指針地址,指向一個新的QSharedDataPointer,命名為data1)
String addr = 0x28c798 , 0x14316660 (str2的指針地址,指向前面同一個QSharedDataPointer,其實就是data1)
String addr = 0x28c798 , 0x1433f2a0 (str2的指針地址,指向一個新的QSharedDataPointer,命名為data2)
String addr = 0x28c798 , 0x1433f2a0 (str2的指針地址,指向data2,但是修改其內容)
String addr = 0x28c79c , 0x1433f2a0 (str1的指針地址,指向data2,不修改其內容,且放棄data1,使之引用計數為零而被徹底釋放)
String addr = 0x28c798 , 0x1433f2a0 (str2的指針地址,指向data2,不修改其內容)
注意,str1的地址和str1.constData()地址不是一回事。
不過新問題又來了,在調用data()函數以后,怎么好像constData的地址也變了:
void MainWindow::on_pushButton_8_clicked() { QString str1="data"; qDebug() << " String addr = " << &str1 <<", "<< str1.constData() << ", " << str1.data(); QString str2=str1; //淺拷貝指向同一個數據塊 qDebug() << " String addr = " << &str2 <<", "<< str2.constData() << ", " << str2.data(); str2[3]='e'; //一次深拷貝,str2對象指向一個新的、不同於str1所指向的數據結構 qDebug() << " String addr = " << &str2 <<", "<< str2.constData() << ", " << str2.data(); str2[0]='f'; //不會引起任何形式的拷貝,因為str2指向的數據結構沒有被共享 qDebug() << " String addr = " << &str2 <<", "<< str2.constData() << ", " << str2.data(); str1=str2; //str1指向的數據結構將會從內存釋放掉,str1對象指向str2所指向的數據結構 qDebug() << " String addr = " << &str1 <<", "<< str1.constData() << ", " << str1.data(); qDebug() << " String addr = " << &str2 <<", "<< str2.constData() << ", " << str2.data(); }
輸出結果:
String addr = 0x28c79c , 0x143e6660 , 0x143e6660
String addr = 0x28c798 , 0x14423020 , 0x14423020
String addr = 0x28c798 , 0x14423020 , 0x14423020
String addr = 0x28c798 , 0x14423020 , 0x14423020
String addr = 0x28c79c , 0x143e6660 , 0x143e6660
String addr = 0x28c798 , 0x14423020 , 0x14423020
原因可能是因為這兩句:
1. constData()的注釋:
Note that the pointer remains valid only as long as the string is not modified.
就是調用data()函數以后,string存儲數據的地址被修改了
2. data()的注釋:
Note that the pointer remains valid only as long as the string is not modified by other means.
For read-only access, constData() is faster because it never causes a deep copy to occur.
大概是因為調用data()函數以后,立刻就引起了深拷貝,從而存儲數據的地址變化了
所以事實上,先調用constData還是先調用data,結果會有所不同:
void MainWindow::on_pushButton_8_clicked() { QString str1="data"; qDebug() << " String addr = " << &str1 <<", "<< str1.data() << ", " << str1.constData(); QString str2=str1; //淺拷貝指向同一個數據塊 qDebug() << " String addr = " << &str2 <<", "<< str2.data() << ", " << str2.constData(); str2[3]='e'; //一次深拷貝,str2對象指向一個新的、不同於str1所指向的數據結構 qDebug() << " String addr = " << &str2 <<", "<< str2.data() << ", " << str2.constData(); str2[0]='f'; //不會引起任何形式的拷貝,因為str2指向的數據結構沒有被共享 qDebug() << " String addr = " << &str2 <<", "<< str2.data() << ", " << str2.constData(); str1=str2; //str1指向的數據結構將會從內存釋放掉,str1對象指向str2所指向的數據結構 qDebug() << " String addr = " << &str1 <<", "<< str1.data() << ", " << str1.constData(); qDebug() << " String addr = " << &str2 <<", "<< str2.data() << ", " << str2.constData(); }
結果(其中constData是想要的結果,值得研究的地方)。而data函數因為深拷貝的原因產生了一個數據的新地址,大概是拷貝到新的存儲空間吧,而constData始終指向這個QString真正存儲數據的地方:
String addr = 0x28c79c , 0x144b3598 , 0x144b3598
String addr = 0x28c798 , 0x14503cc8 , 0x144b3598
String addr = 0x28c798 , 0x14503cc8 , 0x14503cc8
String addr = 0x28c798 , 0x14503cc8 , 0x14503cc8
String addr = 0x28c79c , 0x144b3598 , 0x14503cc8
String addr = 0x28c798 , 0x14503cc8 , 0x14503cc8
要是先調用constData,后調用data,結果這下constData和data又完全一致了:
String addr = 0x28c79c , 0x146b6c28 , 0x146b6c28
String addr = 0x28c798 , 0x14653498 , 0x14653498
String addr = 0x28c798 , 0x14653498 , 0x14653498
String addr = 0x28c798 , 0x14653498 , 0x14653498
String addr = 0x28c79c , 0x146b6c28 , 0x146b6c28
String addr = 0x28c798 , 0x14653498 , 0x14653498
之所以出現這種怪問題,想了半天,覺得是因為data()和constData()寫在同一句語句里的原因,編譯器把全部值算出來以后,再進行打印,這樣constData的值有時候就不准確了。所以最好分成兩句:
void MainWindow::on_pushButton_8_clicked() { QString str1="data"; qDebug() << " String addr = " << &str1 <<", "<< str1.constData(); qDebug() << "new addr = " << str1.data(); QString str2=str1; //淺拷貝指向同一個數據塊 qDebug() << " String addr = " << &str2 <<", "<< str2.constData(); qDebug() << "new addr = " << str2.data(); str2[3]='e'; //一次深拷貝,str2對象指向一個新的、不同於str1所指向的數據結構 qDebug() << " String addr = " << &str2 <<", "<< str2.constData(); qDebug() << "new addr = " << str2.data(); str2[0]='f'; //不會引起任何形式的拷貝,因為str2指向的數據結構沒有被共享 qDebug() << " String addr = " << &str2 <<", "<< str2.constData(); qDebug() << "new addr = " << str2.data(); str1=str2; //str1指向的數據結構將會從內存釋放掉,str1對象指向str2所指向的數據結構 qDebug() << " String addr = " << &str1 <<", "<< str1.constData(); qDebug() << "new addr = " << str1.data(); qDebug() << " String addr = " << &str2 <<", "<< str2.constData(); qDebug() << "new addr = " << str2.data(); }
輸出結果(排版了一下,取消換行):
String addr = 0x28c70c , 0x13c06660 , new addr = 0x13c06660
String addr = 0x28c708 , 0x13c06660 , new addr = 0x13c841b8
String addr = 0x28c708 , 0x13c841b8 , new addr = 0x13c841b8
String addr = 0x28c708 , 0x13c841b8 , new addr = 0x13c841b8
String addr = 0x28c70c , 0x13c841b8 , new addr = 0x13c06660
String addr = 0x28c708 , 0x13c841b8 , new addr = 0x13c841b8
這樣就又正確了,真是煩死人。后面還有沒有坑不知道,今天就到這里為止吧。
參考:http://www.cnblogs.com/wiessharling/archive/2013/01/05/2845819.html
--------------------------------------------------------------------
再來一個例子:
QString str1 = "ubuntu"; QString str2 = str1;//str2 = "ubuntu" str2[2] = "m";//str2 = "ubmntu",str1 = "ubuntu" str2[0] = "o";//str2 = "obmntu",str1 = "ubuntu" str1 = str2;//str1 = "obmntu",
line1: 初始化一個內容為"ubuntu"的字符串;
line2: 將字符串對象str1賦值給另外一個字符串str2(由QString的拷貝構造函數完成str2的初始化)。
在對str2賦值的時候,會發生一次淺拷貝,導致兩個QString對象都會指向同一個數據結構。該數據結構除了保存字符串“ubuntu”之外,還保存一個引用計數器,用來記錄字符串數據的引用次數。此處,str1和str2都指向同一數據結構,所以此時引用計數器的值為2.
line3: 對str2做修改,將會導致一次深拷貝,使得對象str2指向一個新的、不同於str1所指的數據結構(該數據結構中引用計數器值為1,只有str2是指向該結構的),同時修改原來的、str1所指向的數據結構,設置它的引用計數器值為1(此時只有str1對象指向該結構);並在這個str2所指向的、新的數據結構上完成數據的修改。引用計數為1就意味着該數據沒有被共享。
line4: 進一步對str2做修改,不過不會引起任何形式的拷貝,因為str2所指向的數據結構沒有被共享。
line5: 將str2賦給str1.此時,str1修改它指向的數據結構的引用計數器的值位0,表示沒有QString類的對象再使用這個數據結構了;因此str1指向的數據結構將會從從內存中釋放掉;這一步操作的結構是QString對象str1和str2都指向了字符串為“obmntu”的數據結構,該結構的引用計數為2.
Qt中支持引用計數的類有很多(QByteArray, QBrush, QDir, QBitmap... ...).
參考:http://blog.chinaunix.net/uid-27177626-id-3949985.html
--------------------------------------------------------------------
再來一個例子:
int main(int argc, char *argv[]) { QList<QString> list1; list1<<"test"; QList<QString> list2=list1; qDebug()<<&list1.at(0); qDebug()<<&list2.at(0); //qDebug()<<&list1[0]; //[]運算 //qDebug()<<&list2[0]; //[]運算 list2<<"tests"; // 注意,此時list2的內容是("test", "tests") qDebug()<<&list1.at(0); qDebug()<<&list2.at(0); // 之所以這里的地址變得不一致,是因為它的第一項內容地址變了,但仍指向"test"字符串,這里解釋的還不夠清楚。 QList<QString> list=copyOnWrite(); qDebug()<<&list; qDebug()<<&list.at(0); } QList<QString> copyOnWrite() { QList<QString> list; list<<"str1"<<"str2"; ///... qDebug()<<&list; qDebug()<<&list.at(0); return list; }
輸出結果:
0x13df5e28
0x13df5e28
0x13df5e28
0x13d95fa0
0x28c79c
0x13d900c0
0x28c79c
0x13d900c0
1. 網上都說是copyOnWrite函數體內&list地址與主函數中&list地址是一樣的,結果卻是不一致的,但元素地址是一致的,難道錯了?理論上,兩個list自身的地址應該是不一樣的,為什么會結果一樣呢?難道是windows銷毀前一個list后,湊巧又給后一個list重新分配了一模一樣的地址?這與QList使用隱式共享有關系嗎?不明白。補充,好像明白了:是因為返回值又產生一個新的隱式共享,對這個list的引用值增加1,既然是賦值,那么導致函數外面那個新的list也使用這個隱式共享,相當於返回值list充當了中介,然后立即減少它的引用值,這樣函數內的list始終沒有機會被銷毀,導致最后的list使用了前面同一個list,此時其引用數為1。
2. 使用[]運算,數據結構經過復制,不再隱式共享。(在只讀的情況下,使用at()方法要比使用[]運算子效率高,因為省去了數據結構的復制成本)。
參考:http://blog.csdn.net/yestda/article/details/17893221
------------------------------------------------------------------
理論知識:
凡是支持隱式數據共享的 Qt 類都支持類似的操作。用戶甚至不需要知道對象其實已經共享。因此,你應該把這樣的類當作普通類一樣,而不應該依賴於其共享的特色作一些“小動作”。事實上,這些類的行為同普通類一樣,只不過添加了可能的共享數據的優點。因此,你大可以使用按值傳參,而無須擔心數據拷貝帶來的性能問題。
注意,前面已經提到過,不要在使用了隱式數據共享的容器上,在有非 const STL 風格的遍歷器正在遍歷時復制容器。另外還有一點,對於QList或者QVector,我們應該使用at()函數而不是 [] 操作符進行只讀訪問。原因是 [] 操作符既可以是左值又可以是右值,這讓 Qt 容器很難判斷到底是左值還是右值,這意味着無法進行隱式數據共享;而at()函數不能作左值,因此可以進行隱式數據共享。另外一點是,對於begin(),end()以及其他一些非 const 遍歷器,由於數據可能改變,因此 Qt 會進行深復制。為了避免這一點,要盡可能使用const_iterator、constBegin()和constEnd()。
參考:http://jukezhang.com/2014/11/23/learn-qt-eight/
--------------------------------------------------------------------
總結:到今天我才算明白,什么是引用計數。一定要對某個QString經過賦值過程(=)以后,才會增加引用計數,或者發生copyOnWrite。而不是說天馬行空給一個新字符串直接賦值,比如執行一句QString str1="aaaa",這種情況下,即使另一個字符串str2剛巧目前也是"aaaa",也不會對str2產生增加引用計數,而是創造一個新的字符串"aaaa"在內存中,此時str1和str2分別指向不同地址的字符串,盡管其內容相同,但它們的引用計數都是1。更不是當執行QString str1="aaaa"的時候,在當前程序的全部內存里搜索有沒有另一個字符串的內容剛好是"aaaa",然后給它增加引用計數,沒有的話,才創造一個新的"aaaa"字符串(那樣效率會多么低啊,雖然也有點納悶,但以前我就是這樣理解的)。Delphi里也是同理,以前不明白,現在明白了。
附加總結1(關於字符串):在函數內定義一個QString str1,這個str1僅僅代表一個字符串指針而已,雖然str1指針是在stack上分配的,但其真正的字符串內容仍然是存儲在heap里的。sizeof(str1)=4也可證明這一點,無論str1是什么值都是這樣。同時sizeof(QString)=4,永遠都是這樣。經測試,Delphi里也完全如此!因為兩者都是采用了引用計數的方法嘛!既然引用計數,就不能是當場分配全部的內存空間存儲用來存儲全部的數據,而只能是現在這個樣子。
附加總結2(關於指針):上面第三個例子的list,說明它的地址不是當場在stack或者heap里分配的,而是之前內存里就存在的一個地址。這對我對指針有了新的理解——不是什么指針都是新分配的,要看這個數據類型是不是具有隱式共享的特征,如果是,就要小心,它不一定分配新的內存地址,僅僅是指針地址也不會分配!
最后附上整個項目文件:http://files.cnblogs.com/files/findumars/testmem.rar
--------------------------------------------------------------------
最后就是好奇,在編譯器不是自己做的情況下(Delphi的字符串引用計數是在編譯器級實現的),如何實現隱式共享的。想了想,應該是重載operator =,全都返回一個引用,查了一下果然如此(除了QCache):
http://doc.qt.io/qt-4.8/implicit-sharing.html
為了增加對引用的理解,做了一個小例子:
void MainWindow::on_pushButton_10_clicked() { int a=999; int& b = a; qDebug() << &a <<", "<< &b; }
輸出結果:
0x28d350 , 0x28d350
兩個變量的地址值果然完全是一致的。這里特別強調,引用並不產生對象的副本,僅僅是對象的同義詞。另外提一句,在引用當作參數傳給函數的時候,引用的本質就是間接尋址。
為了進一步加深大家對指針和引用的區別,下面我從編譯的角度來闡述它們之間的區別:
程序在編譯時分別將指針和引用添加到符號表上,符號表上記錄的是變量名及變量所對應地址。指針變量在符號表上對應的地址值為指針變量的地址值,而引用在符號表上對應的地址值為引用對象的地址值。符號表生成后就不會再改,因此指針可以改變其指向的對象(指針變量中的值可以改),而引用對象則不能修改。
參考:http://www.cnblogs.com/yanlingyin/archive/2011/12/07/2278961.html
--------------------------------------------------------------------
最后再來一個例子,兩者使用內存的區別十分明顯:
void MainWindow::on_pushButton_2_clicked()
{
QStringList list;
for (int i=0; i<5000000; i++) { QString str = "aaaa"; list << str; } QMessageBox::question(NULL, "Test", "finish", QMessageBox::Yes); }
和
void MainWindow::on_pushButton_3_clicked()
{
QStringList list;
QString str = "aaaa"; for (int i=0; i<5000000; i++) { list << str; } QMessageBox::question(NULL, "Test", "finish", QMessageBox::Yes); }
兩段代碼會使用完全不同的內存大小。因為第一個程序在內存里產生了5百萬個"aaaa"字符串,使用內存多達220M,而第二個程序在內存中只有一個字符串"aaaa",但這個字符串的引用計數在不斷地變化直至500萬,運行后發現只使用了25M左右的內存,這就是引用計數的魅力。