浅拷贝,深拷贝,隐式共享的三个例子

1.浅拷贝:
浅拷贝就比如像引用类型
浅拷贝是指源对象与拷贝对象共用一份实体,仅仅是引用的变量不同(名称不同) 。对其中任何一个对象的改动都会影响另外一个对象 。举个例子,一个人一开始叫张三,后来改名叫李四了,可是还是同一个人,不管是张三缺胳膊少腿还是李四缺胳膊少腿,都是这个人倒霉 。
2.深拷贝:
而深拷贝就比如值类型 。
深拷贝是指源对象与拷贝对象互相独立,其中任何一个对象的改动都不会对另外一个对象造成影响 。举个例子 , 一个人名叫张三,后来用他克?。偕璺稍市恚┝肆硗庖桓鋈耍欣钏?,不管是张三缺胳膊少腿还是李四缺胳膊少腿都不会影响另外一个人 。比较典型的就是Value(值)对象,如预定义类型Int32,,以及结构() , 枚举(Enum)等 。
3.隐式共享:
隐式共享又叫做回写复制 。当两个对象共享同一份数据时(通过浅拷贝实现数据块的共享),如果数据不改变,不进行数据的复制 。而当某个对象需要改变数据时则执行深拷贝 。
类采用隐式共享技术,将深拷贝和浅拷贝有机地结合起来 。
例如:
【浅拷贝,深拷贝,隐式共享的三个例子】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();}
实测输出结果如下(括号内是我的分析):
addr =,(str1的指针地址,指向一个新的,命名为data1)
addr =,(str2的指针地址 , 指向前面同一个,其实就是data1)
addr =,(str2的指针地址,指向一个新的,命名为data2)
addr =,(str2的指针地址,指向data2,但是修改其内容)
addr =,(str1的指针地址,指向data2,不修改其内容 , 且放弃data1,使之引用计数为零而被彻底释放)
addr =,(str2的指针地址,指向data2,不修改其内容)
注意 , str1的地址和str1.()地址不是一回事 。
不过新问题又来了,在调用data()函数以后,怎么好像的地址也变了:
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();}
输出结果:
addr =,,
addr =,,
addr =,,
addr =,,
addr =,,
addr =,,
原因可能是因为这两句:
1. ()的注释:
Note that thevalid only as long as theis not .
就是调用data()函数以后,存储数据的地址被修改了
2. data()的注释:
Note that thevalid only as long as theis notby other means.
For read-only , () isit nevera deep copy to occur.
大概是因为调用data()函数以后,立刻就引起了深拷贝,从而存储数据的地址变化了
所以事实上,先调用还是先调用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();}
结果(其中是想要的结果,值得研究的地方) 。而data函数因为深拷贝的原因产生了一个数据的新地址,大概是拷贝到新的存储空间吧,而始终指向这个真正存储数据的地方:
addr =,,
addr =,,
addr =,,
addr =,,
addr =,,
addr =,,
要是先调用,后调用data,结果这下和data又完全一致了:
addr =,,
addr =,,
addr =,,
addr =,,
addr =,,
addr =,,
之所以出现这种怪问题,想了半天,觉得是因为data()和()写在同一句语句里的原因,编译器把全部值算出来以后,再进行打印,这样的值有时候就不准确了 。所以最好分成两句:
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();}
输出结果(排版了一下,取消换行):
addr =,, new addr =
addr =,, new addr =
addr =,, new addr =
addr =,, new addr =
addr =,, new addr =
addr =,, new addr =
这样就又正确了,真是烦死人 。后面还有没有坑不知道,今天就到这里为止吧 。
参考:
--------------------------------------------------------------------
再来一个例子:
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: 初始化一个内容为""的字符串;
line2: 将字符串对象str1赋值给另外一个字符串str2(由的拷贝构造函数完成str2的初始化) 。
在对str2赋值的时候,会发生一次浅拷贝,导致两个对象都会指向同一个数据结构 。该数据结构除了保存字符串“”之外,还保存一个引用计数器,用来记录字符串数据的引用次数 。此处,str1和str2都指向同一数据结构,所以此时引用计数器的值为2.
line3: 对str2做修改,将会导致一次深拷贝,使得对象str2指向一个新的、不同于str1所指的数据结构(该数据结构中引用计数器值为1 , 只有str2是指向该结构的),同时修改原来的、str1所指向的数据结构,设置它的引用计数器值为1(此时只有str1对象指向该结构);并在这个str2所指向的、新的数据结构上完成数据的修改 。引用计数为1就意味着该数据没有被共享 。
line4:进一步对str2做修改,不过不会引起任何形式的拷贝 , 因为str2所指向的数据结构没有被共享 。
line5: 将str2赋给str1.此时,str1修改它指向的数据结构的引用计数器的值位0,表示没有类的对象再使用这个数据结构了;因此str1指向的数据结构将会从从内存中释放掉;这一步操作的结构是对象str1和str2都指向了字符串为“”的数据结构,该结构的引用计数为2.
Qt中支持引用计数的类有很多(, , QDir, ... ...).
参考:
--------------------------------------------------------------------
再来一个例子:
int main(int argc, char *argv[]){QList list1;list1<<"test";QList 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 list=copyOnWrite();qDebug()<<&list;qDebug()<<&list.at(0);}QList copyOnWrite(){QList list;list<<"str1"<<"str2";///...qDebug()<<&list;qDebug()<<&list.at(0);return list;}

浅拷贝,深拷贝,隐式共享的三个例子

文章插图
输出结果:
1. 网上都说是函数体内&list地址与主函数中&list地址是一样的,结果却是不一致的,但元素地址是一致的,难道错了?理论上,两个list自身的地址应该是不一样的,为什么会结果一样呢?难道是销毁前一个list后,凑巧又给后一个list重新分配了一模一样的地址?这与QList使用隐式共享有关系吗?不明白 。补充 , 好像明白了:是因为返回值又产生一个新的隐式共享,对这个list的引用值增加1,既然是赋值,那么导致函数外面那个新的list也使用这个隐式共享,相当于返回值list充当了中介,然后立即减少它的引用值,这样函数内的list始终没有机会被销毁,导致最后的list使用了前面同一个list,此时其引用数为1 。
2.使用[]运算,数据结构经过复制,不再隐式共享 。(在只读的情况下,使用at()方法要比使用[]运算子效率高,因为省去了数据结构的复制成本) 。
参考:
------------------------------------------------------------------
理论知识:
凡是支持隐式数据共享的 Qt 类都支持类似的操作 。用户甚至不需要知道对象其实已经共享 。因此,你应该把这样的类当作普通类一样,而不应该依赖于其共享的特色作一些“小动作” 。事实上,这些类的行为同普通类一样,只不过添加了可能的共享数据的优点 。因此,你大可以使用按值传参,而无须担心数据拷贝带来的性能问题 。
注意,前面已经提到过,不要在使用了隐式数据共享的容器上,在有非 const STL 风格的遍历器正在遍历时复制容器 。另外还有一点,对于QList或者,我们应该使用at()函数而不是 [] 操作符进行只读访问 。原因是 [] 操作符既可以是左值又可以是右值,这让 Qt 容器很难判断到底是左值还是右值,这意味着无法进行隐式数据共享;而at()函数不能作左值,因此可以进行隐式数据共享 。另外一点是,对于begin(),end()以及其他一些非 const 遍历器 , 由于数据可能改变,因此 Qt 会进行深复制 。为了避免这一点,要尽可能使用、()和() 。
参考:
--------------------------------------------------------------------
总结:到今天我才算明白 , 什么是引用计数 。一定要对某个经过赋值过程(=)以后,才会增加引用计数,或者发生 。而不是说天马行空给一个新字符串直接赋值,比如执行一句 str1="aaaa",这种情况下 , 即使另一个字符串str2刚巧目前也是"aaaa",也不会对str2产生增加引用计数,而是创造一个新的字符串"aaaa"在内存中,此时str1和str2分别指向不同地址的字符串,尽管其内容相同,但它们的引用计数都是1 。更不是当执行 str1="aaaa"的时候 , 在当前程序的全部内存里搜索有没有另一个字符串的内容刚好是"aaaa",然后给它增加引用计数,没有的话 , 才创造一个新的"aaaa"字符串(那样效率会多么低啊,虽然也有点纳闷,但以前我就是这样理解的) 。里也是同理 , 以前不明白,现在明白了 。
附加总结1(关于字符串):在函数内定义一个 str1,这个str1仅仅代表一个字符串指针而已,虽然str1指针是在stack上分配的,但其真正的字符串内容仍然是存储在heap里的 。(str1)=4也可证明这一点,无论str1是什么值都是这样 。同时()=4,永远都是这样 。经测试,里也完全如此!因为两者都是采用了引用计数的方法嘛!既然引用计数,就不能是当场分配全部的内存空间存储用来存储全部的数据,而只能是现在这个样子 。
附加总结2(关于指针):上面第三个例子的list,说明它的地址不是当场在stack或者heap里分配的,而是之前内存里就存在的一个地址 。这对我对指针有了新的理解——不是什么指针都是新分配的 , 要看这个数据类型是不是具有隐式共享的特征 , 如果是,就要小心,它不一定分配新的内存地址,仅仅是指针地址也不会分配!
最后附上整个项目文件:
--------------------------------------------------------------------
最后就是好奇,在编译器不是自己做的情况下(的字符串引用计数是在编译器级实现的),如何实现隐式共享的 。想了想 , 应该是重载 =,全都返回一个引用,查了一下果然如此(除了):
为了增加对引用的理解,做了一个小例子:
void MainWindow::on_pushButton_10_clicked(){int a=999;int& b = a;qDebug() << &a <<", "<< &b;}
输出结果:
,
两个变量的地址值果然完全是一致的 。这里特别强调,引用并不产生对象的副本,仅仅是对象的同义词 。另外提一句,在引用当作参数传给函数的时候 , 引用的本质就是间接寻址 。
为了进一步加深大家对指针和引用的区别,下面我从编译的角度来阐述它们之间的区别:
程序在编译时分别将指针和引用添加到符号表上,符号表上记录的是变量名及变量所对应地址 。指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值 。符号表生成后就不会再改 , 因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改 。
参考:
--------------------------------------------------------------------
最后再来一个例子,两者使用内存的区别十分明显:
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左右的内存,这就是引用计数的魅力 。