浅拷贝,深拷贝,隐式共享的三个例子——有3个重要总结,意外发现同一句语句执行2个函数居然还会相互干扰
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里分配的,而是之前内存里就存在的一个地址。这对我对指针有了新的理解——不是什么指针都是新分配的,要看这个数据类型是不是具有隐式共享的特征,如果是,就要小心,它不一定分配新的内存地址,仅仅是指针地址也不会分配!
最后附上整个项目文件:https://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左右的内存,这就是引用计数的魅力。