首页 >> 大全

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

2023-12-29 大全 27 作者:考证青年

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 the valid only as long as the is not .

就是调用data()函数以后,存储数据的地址被修改了

2. data()的注释:

Note that the valid only as long as the is not by other means.

For read-only , () is it never a 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左右的内存,这就是引用计数的魅力。

关于我们

最火推荐

小编推荐

联系我们


版权声明:本站内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 88@qq.com 举报,一经查实,本站将立刻删除。备案号:桂ICP备2021009421号
Powered By Z-BlogPHP.
复制成功
微信号:
我知道了