为了提升性能,Qt框架里面使用了隐式共享技术(基于d-p模式),也可以理解为写时复制技术(COW)。在开发过程中我们一般不需要去关系隐式共享的底层运行机制,但是了解一下还是有必要的。 关于隐式共享,网上有很多相关介绍的文章,这里就不多赘述。写这篇文章的目的,只是记录一下这几天在项目上因隐式共享造成的一个BUG。相较于文档之类的理论只是,结合实际项目中的问题来理解隐式共享,可能会有更深的体会。
先看一下以下代码:
//定义一个Data数据结构,包含一个成员x
struct Data {
int x = 10;
};
void func() {
//声明一个Data的链表
QList<Data> myList;
//给链表添加一个x为10的Data元素
myList << Data{ 10 };
//声明一个指向链表第一个元素的引用
Data& intRef = myList[0];
//声明一个新的链表myList2,等于myList
QList<Data> myList2 = myList;
//修改myList第一个元素的x
myList[0].x = 20;
//到这里,intRef.x为多少? 10?还是20?
qDebug() << intRef.x; //打印10,而非20
}
qDebug() 打印了intRef.x的值为10! intRef引用了myList[0],然后修改了myList[0].x为20,为什么打印出来intRef.x却为10? 这就是隐式共享造成的结果。
调用myList2 = myList时,发生了隐式共享,也就是说myList2和myList的d指针(d1)是共享的(不了解d-p机制的同学可以自己查阅相关文档),所以intRef引用了d1中的第一个Data元素。 当给myList[0]复制时,会打破这种共享状态,myList会重新创建一个d对象(d2),然后修改d2中第一个Data元素的x为20。 很显然,这时候intRef指向的是myList2的第一个元素,和myList没有任何关系了。
再模拟一下我在项目上碰到的BUG:
//定义一个Data数据结构,包含一个成员x
struct Data {
int x = 10;
};
void func() {
//声明一个Data的链表
QList<Data> myList;
//给链表添加一个x为10的Data元素
myList << Data{ 10 };
//声明一个指向链表第一个元素的引用
Data& intRef = myList[0];
//声明一个新的链表myList2,等于myList
QList<Data> myList2 = myList;
//修改myList第一个元素的x
myList[0].x = 20;
//主动析构myList2
myList2.~QList();
//到这里,intRef指向的对象还存在吗?
intRef.x = 1000;
}
相较上面的代码,这里加了一行myList2.~QList(),显示调用QList的析构函数。因为intRef引用的是myList2的第一个元素,myList2析构后,链表里面的Data元素也被析构了,所以后面再给intRef.x复制,就可能造成堆栈内存的破坏。
所以,将引用绑定到一个具有隐式共享(写时复制)的容器对象中的元素时,是比较危险的,一定要注意!