首页 Qt 学习之路 2 Qt 学习之路 2(40):隐式数据共享

Qt 学习之路 2(40):隐式数据共享

15 2.2K

Qt 中许多 C++ 类使用了隐式数据共享技术,来最大化资源利用率和最小化拷贝时的资源消耗。当作为参数传递时,具有隐式数据共享的类即安全又高效。在数据传递时,实际上只是传递了数据的指针(这一切都是隐含帮你完成的),而只有在函数发生需要写入的情况时,数据才会被拷贝(也就是通常所说的写时复制)。本章我们将介绍有关隐式数据共享的相关内容,以便为恰当地使用前面所介绍的容器夯实基础。

具有数据共享能力的类包含了一个指向共享数据块的指针。这个数据块包含了数据本身以及数据的引用计数。当共享对象创建出来时,引用计数被设置为 1。当新的对象引用到共享数据时,引用计数增加;当对象引用不再引用数据时,引用计数减少。当引用计数变为 0 时,共享数据被删除。

在我们操作共享数据时,实际有两种拷贝对象的方法:我们通常称其为深拷贝和浅拷贝。深拷贝意味着要重新构造一个全新的对象;浅拷贝则仅仅复制引用,也就是上面所说的那个指向共享数据块的指针。深拷贝对内存和 CPU 资源都是很昂贵的;浅拷贝则非常快速,因为它仅仅是设置一个新的指针,然后将引用计数加 1。具有隐式数据共享的对象,其赋值运算符使用的是浅拷贝来实现的。

这种隐式数据共享的好处是,程序不需要拥有不必要的重复数据,减少数据拷贝的需求。重复数据的代价是降低内存使用率(因为内存存储了更多重复的数据)。通过数据共享,对象可以更简单地作为值来传递以及从函数中返回。

隐式数据共享是在底层自动完成的,程序人员无需关心。这也是“隐式”一词的含义。从 Qt4 开始,即使在多线程程序中,隐式数据共享也是起作用的。在很多人看来,隐式数据共享和多线程是不兼容的,这是由引用计数的实现方式决定的。但是,Qt 使用了原子性的引用计数来避免多线程环境下可能出现的执行顺序打断的行为。需要注意的是,原子引用计数并不能保证线程安全,还是需要恰当的锁机制。这种观点对所有类似的场合都是适用的。原子引用计数能够保证的是,线程肯定操作自己的数据,线程自己的数据是安全的。总的来说,从 Qt4 开始,你可以放心使用隐式数据共享的类,即使在多线程环境下。

我们可以使用QSharedDataQSharedDataPointer类实现自己的隐式数据共享类。

当对象即将被修改,并且其引用计数大于 1 时,隐式数据共享自动将数据从共享块中拿出。隐式共享类必须控制其内部数据,在任何修改其数据的函数中,将数据自动取出。

QPen使用了隐式数据共享技术,我们以QPen为例,看看隐式数据共享是如何起作用的:

void QPen::setStyle(Qt::PenStyle style)
{
    detach(); // 从共享区取出数据
    d->style = style; // 设置数据(更新)
}

void QPen::detach()
{
    if (d->ref != 1) {
        ... // 执行深拷贝
    }
}

凡是支持隐式数据共享的 Qt 类都支持类似的操作。用户甚至不需要知道对象其实已经共享。因此,你应该把这样的类当作普通类一样,而不应该依赖于其共享的特色作一些“小动作”。事实上,这些类的行为同普通类一样,只不过添加了可能的共享数据的优点。因此,你大可以使用按值传参,而无须担心数据拷贝带来的性能问题。例如:

QPixmap p1, p2;
p1.load("image.bmp");
p2 = p1; // p1 和 p2 共享数据

QPainter paint;
paint.begin(&p2); // 从此,p2 与 p1 分道扬镳
paint.drawText(0,50, "Hi");
paint.end();

上例中,p1 和 p2 在QPainter::begin()一行之前都是共享数据的,直到这一语句。因为该语句开始,p2 就要被修改了。

注意,前面已经提到过,不要在使用了隐式数据共享的容器上,在有非 const STL 风格的遍历器正在遍历时复制容器。另外还有一点,对于QList或者QVector,我们应该使用at()函数而不是 [] 操作符进行只读访问。原因是 [] 操作符既可以是左值又可以是右值,这让 Qt 容器很难判断到底是左值还是右值,这意味着无法进行隐式数据共享;而at()函数不能作左值,因此可以进行隐式数据共享。另外一点是,对于begin()end()以及其他一些非 const 遍历器,由于数据可能改变,因此 Qt 会进行深复制。为了避免这一点,要尽可能使用const_iteratorconstBegin()constEnd()

15 评论

panda 2013年1月23日 - 21:42

你好,能帮我解答一个疑惑吗?
在 C++ GUI Programming with Qt 里关于QTableWidget有这么一段描述,说是当 QTableWidget 的 QTableWidgetItems 被 delte 时,它会注意到并且会自动地重绘自己。可是 QTableWidgetItem 并没有继承 QWidget 呀,它没有 parent 属性,怎么通知 QTableWidget 呢?

回复
豆子 2013年1月24日 - 09:18

并不一定必须要有显式的 parent 参数(或者继承 QWidget)才能够做到这一点。注意到 QTableWidgetItem 有一个函数 tableWidget () const 可以返回其所在的 QTableWidget 组件,因此在添加的时候,肯定会以某种机制是其知道自己的父组件。因此这二者并不矛盾。

回复
panda 2013年1月24日 - 09:46

Thanks!
嗯,基本上可以断定所有 QWidget 的子类以及像 QTableWidgetItem 这样的类在被 delete 的时候,都会自动被从父组件中移除,这种机制应该是写在析构函数中的,这样理解没错吧?

回复
豆子 2013年1月25日 - 09:08

应该是这样的,具体实现还是要看代码

回复
科尔曼 2013年1月25日 - 09:53

Qt助手和Qt源码(当前5.0.0)都是不错的辅助工具。可以查看详细信息。

回复
豆子 2013年1月25日 - 14:18

是的,这些内容大部分都可以在帮助文档里面找到的

回复
迷路的马瑞 2013年4月18日 - 09:51

如果我没记错的化,目前的Qt使用[]也不会有问题,比如QString,左值版本的operator[]返回的是QCharRef,QCharRef在调用opeartor=的时候,才会去进行深拷贝

回复
豆子 2013年4月19日 - 14:05

对于 [],这里是指出如果使用 [],会由于 Qt 无法分清 [] 运算符是左值还是右值而导致无法进行隐式数据共享,并不是因为会进行深拷贝。

回复
马小巾 2013年7月25日 - 11:01

豆子兄,当界面有两个头文件如mainwindow.h和function.h,function.h与function.cpp中定义函数,mainwindow中调用并输出结果,但mainwindow.cpp调用时总报“非静态函数的非法调用”,一直调不好,想请教一下你的高见 💡

回复
豆子 2013年7月25日 - 12:56

可能是你使用类名调用成员函数了

回复
Const_Lin 2014年3月16日 - 11:11

豆子你好:
”不要在使用了隐式数据共享的容器上,在有非 const STL 风格的遍历器正在遍历时复制容器“
不知道可以举一个具体的例子?一直搞不懂

回复
豆子 2014年3月17日 - 09:25

简单来说,就是如果有一个非 const 的 STL 风格遍历器正在遍历容器的时候,此时不要试图复制这个容器。一般而言,如果容器需要共享,或者有多线程同时访问时,出现这种问题的可能性更大,其余一般不容易出现这种问题。

回复
bdss58 2015年1月24日 - 14:53

哎~~~还是没看懂。c++ STL容器的对象拷贝这方面基础不好啊~~

回复
dreamychi 2015年11月12日 - 17:07

感觉是使用at()还是[]是没有任何区别,因为QList的源码实现里,只有一个Q_Assert输出的字符串不一样,底层用的都是QListData的at()方法。这和隐式共享,写时拷贝这些好像没啥关系啊

回复
Nephren 2021年7月1日 - 09:22

回应 dreamychi 的问题,作者说了operator[]因为有重载,返回const T&的版本和at()一致,但是 返回 T& 的版本会detach,如果容器不是const的,那么就会调用后者,就会涉及深拷贝的问题。

回复

回复 马小巾 取消回复

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2