[转]QVector与QByteArray——Qt的写时复制(copy on write)技术
我们在之前的博文QVector的内存分配策略与再谈QVector与std::vector——使用装饰者让std::vector支持连续赋值中简单聊了聊QVector内存分配和赋值方面的一点东西,今天接着从QVector展开谈谈Qt的写时复制技术。老实说,“隐式共享,引用计数,写时复制”也是老调重弹的话题了,不过也是QTL与STL最大的区别之一,这篇博文不详谈“写时复制”技术的细节,那个有不少文章介绍过了,我们扯点关于operator[]和QByteRef和QStringRef的犊子就好。
废话不多说,直接进主题,我们先从一段最普通的代码开始:
QVector<int> v1;
v1 << 1 << 2 << 3 << 4 << 5;
QVector<int> v2(v1); // 此时v2与v1共享数据(内存)
v2[1] = 8; // 写动作产生,v2被分配新内存
qDebug() << v1 << endl << v2;
利用"写时复制"技术,v1“复制”给v2时并没有立刻复制,v2只是指向v1的内存地址,只有当v2被修改时才真正为其分配新的内存。 这样可以避免一些不必要的内存浪费和构造析构开销。得益于Qt良好的封装,即使我们不知道这一切,依然享受着“写时复制”机制带给我们的好处。但是如果对底层的不了解,又不经意间写下了这样的代码,可能就会对输出结果感到好奇了:
QVector<int> v1;
v1 << 1 << 2 << 3 << 4 << 5;
int *p = &v1[1]; // 声明一个指针指向 v1 的第二个数据
QVector<int> v2(v1); // 此时v2与v1共享数据(内存)
*p = 8; // 使用指针对 v1 数据进行修改
qDebug() << v1 << endl << v2;
如你所见的是,我们用指向v1的指针修改v1,结果v1与v2的数据都被改变了。原因就在于“利用指针修改内存值”这种写行为无法被QVector类侦测到,因而不能触发其复制机制,当我们在使用qDebug输出v1与v2的值时,他们两者依然共享着同一段内存,因此输出相同的结果。而这种结果大多数情况下都不是程序员想要的,编写Qt代码应该十分小心这个问题
那有没有解决办法?很显然的我们似乎只有从重载operator[]入手,但是这个操作符比较特殊,比如在
int i = v2[1];
所示的情况下,作为只读情况,我们不需要让v2独立出来,因此无需复制。
而在下面这种情况,作为写入的情况,v2中的内容即将被修改,因此需要马上复制出自己一份独立的数据出来。
v2[1] = 10;
剩下还有一种就上面所示的情况了,v1[1]被指针定位,我们根本不能确定用户取到它之后会不会修改它,如果不修改而我们又在operator[]内做了复制工作岂不是浪费;如果修改了,我们却在当时没做复制工作,之后就没机会了,就像我们上面看到的例子一样。
遗憾的是,C++并不能区分[]符是在以上哪种情况中被调用的,一概复制可能会浪费,一概不复制又会出问题,怎么做呢,QByteArray的设计给了我们答案(QString类似,至于QVector等容器并未采用此方法的原因,后述)。
我们知道对QByteArray调用[]会返回一个char,因此可以写段类似的代码看看:
QByteArray str1("HelloWorld");
char *c = &str1[2];
QByteArray str2(str1);
*c = 'M';
qDebug() << str1 << " " << str2;
我们同样定义一个char*指针,可是这次报错了:
"cannot convert 'QByteRef*' to 'char*' in initializaion"(QString 则是 QStringRef)
咦,好像类型不匹配,姑且不管他是个什么东西,我们把第二行改成这样让它匹配:
QByteRef *c = &str1[2];
还是有报错:"taking address of temporary"
错误字面解释很清楚了,我们试图获取的这个东西,在返回之前就释放了,当然也就不能取引用。所以对于这个返回对象,我们需要让它产生一个复制行为,只能这么取:
QByteArray str1("HelloWorld");
QByteRef c = str1[2];
QByteArray str2(str1);
c = 'M';
qDebug() << str1 << " " << str2;
查看打印结果,没有问题,只有str1被修改了。
在考虑QByteRef到底是个什么东西的时候,我们回头来思考之前的问题:虽然我们不能确定operator []是在左值还是右值的情况被调用,但是我们可以让这个函数返回一个代理类(Proxy Class),然后等待看看这个Proxy类如何被运用——如果它被读取,我们就将operator[]的调用视为一个读取动作,如果它被写,我们就将operator[]的调用视为一个写动作,执行复制行为。好吧,正如你所猜想的,QByteRef正是这样的一个代理类。
QByteArray中重载operator[]的代码如下,除了返回一个QByteRef对象之外什么也没做:
inline QByteRef QByteArray::operator[](int i)
{ Q_ASSERT(i >= 0); return QByteRef(*this, i); }
inline QByteRef QByteArray::operator[](uint i)
{ return QByteRef(*this, i); }
而QByteRef作为QByteArray的内嵌类不到20行:
class Q_CORE_EXPORT QByteRef {
QByteArray &a;
int i;
inline QByteRef(QByteArray &array, int idx)
: a(array),i(idx) {}
friend class QByteArray;
public:
inline operator char() const
{ return i < a.d->size ? a.d->data()[i] : char(0); }
inline QByteRef &operator=(char c)
{ if (i >= a.d->size) a.expand(i); else a.detach();
a.d->data()[i] = c; return *this; }
inline QByteRef &operator=(const QByteRef &c)
{ if (i >= a.d->size) a.expand(i); else a.detach();
a.d->data()[i] = c.a.d->data()[c.i]; return *this; }
inline bool operator==(char c) const
{ return a.d->data()[i] == c; }
inline bool operator!=(char c) const
{ return a.d->data()[i] != c; }
inline bool operator>(char c) const
{ return a.d->data()[i] > c; }
inline bool operator>=(char c) const
{ return a.d->data()[i] >= c; }
inline bool operator<(char c) const
{ return a.d->data()[i] < c; }
inline bool operator<=(char c) const
{ return a.d->data()[i] <= c; }
};
现在我们可以轻松地分辨operator []的左值与右值运用了,但当然这也是有的弊端的,原先我们使用v1[1]的方式取出来的就是原始数据类型,比如QVector<int>,我们可以对v1[1]使用+,-,++,--,等等操作符,如果是QVector<MyClass>还可以调用我们自己定义的member function。但是我们一旦开始使用代理类,如果你不同意,编译器可不会让++,--,<,>等东西施加在QXXRef这种类型上。如果我们还想按原来的方式使用,就得重载一大堆函数了,就如上面的代码中后面的代码所示。
好在呢,QByteArray、QString与其他QTL不同,它们内部总是char类型数据,因此重载char相关的操作符就可以了。而QVector、QList等等这种内部数据类型由用户决定的容器就不方便这么做了,也是解释上面QVector为什么不使用代理类的原因。因此,在使用QVector、QList等模板类时,使用指针修改可能已经被隐式共享的对象时,一定要多加小心。
[转]QVector与QByteArray——Qt的写时复制(copy on write)技术的更多相关文章
- php变量 写时改变 写时复制
写时复制 $var = 1; $var2 = $var; #此时$var2 与 $var 指向同一个zval refcount = 2: $var = 2; # 此时$val 改变 所以 $var 与 ...
- PHP写时复制, 变量复制和对象复制不同!!!
2016年3月18日 15:09:28 星期五 一直以为PHP对象也是写时复制....... 其实: PHP的变量是写时复制, 对象是引用的 写时复制: $a = $b; 如果$b的内容不改变, $a ...
- php 垃圾回收机制----写时复制和引用计数
PHP使用引用计数和写时复制来管理内存.写时复制保证了变量间复制值不浪费内存,引用计数保证了当变量不再需要时,将内存释放给操作系统. 要理解PHP内存管理,首先要理解一个概念----符号表. 符号表的 ...
- JAVA中写时复制(Copy-On-Write)Map实现
1,什么是写时复制(Copy-On-Write)容器? 写时复制是指:在并发访问的情景下,当需要修改JAVA中Containers的元素时,不直接修改该容器,而是先复制一份副本,在副本上进行修改.修改 ...
- fork()和写时复制
写时复制技术最初产生于Unix系统,用于实现一种傻瓜式的进程创建:当发出fork( )系统调用时,内核原样复制父进程的整个地址空间并把复制的那一份分配给子进程.这种行为是非常耗时的,因为它需要: · ...
- Linux的fork()写时复制原则(转)
写时复制技术最初产生于Unix系统,用于实现一种傻瓜式的进程创建:当发出fork( )系统调用时,内核原样复制父进程的整个地址空间并把复制的那一份分配给子进程.这种行为是非常耗时的,因为它需要: · ...
- Linux进程管理——fork()和写时复制
写时复制技术最初产生于Unix系统,用于实现一种傻瓜式的进程创建:当发出fork( )系统调用时,内核原样复制父进程的整个地址空间并把复制的那一份分配给子进程.这种行为是非常耗时的,因为它需要: · ...
- 写时复制和fork,vfork,clone
写时复制 原理: 用了“引用计数”,会有一个变量用于保存引用的数量.当第一个类构造时,string的构造函数会根据传入的参数从堆上分配内存,当有其它类需要这块内存时,这个计数为自动累加,当有类析构时, ...
- c++ string写时复制
string写时复制:将字符串str1赋值给str2后,除非str1的内容已经被改变,否则str2和str1共享内存.当str1被修改之后,stl才为str2开辟内存空间,并初始化. #include ...
随机推荐
- Mac 安装nodejs
原文链接:http://blog.csdn.net/u010053344/article/details/50545304 Mac 安装nodejs 这几日因为需求需要又临时用到nodejs,之前安装 ...
- 解决MySQL复制出错 Last_SQL_Errno:1146
背景:我们在做数据迁移或者拆分的时候,使用Tablespace transcation 这种解决方案时,很有可能就会遇到 从库复制出错,报: Last_SQL_Errno: 1146 那么具体错误内容 ...
- cookie属性和作用
面试面到了cookie,自己只是还是有一点欠缺,找到一篇文章,学习一下 在chrome控制台中的resources选项卡中可以看到cookie的信息. 现在的chrome控制台已经更新了,所以要到Ap ...
- BZOJ 3282: Tree
3282: Tree Time Limit: 30 Sec Memory Limit: 512 MBSubmit: 1714 Solved: 765[Submit][Status][Discuss ...
- 【转载】dfs序七个经典问题
作者:weeping 出处:www.cnblogs.com/weeping/ 原文链接 https://www.cnblogs.com/weeping/p/6847112.html 参考自:<数 ...
- php高效遍历文件夹、高效读取文件
/** * PHP高效遍历文件夹(大量文件不会卡死) * @param string $path 目录路径 * @param integer $level 目录深度 */ function fn_sc ...
- jquery如何实现点击LI标签和下面的LI互换顺序? 超简单代码
转: jquery如何实现点击LI标签和下面的LI互换顺序? 上面的效果涉及jquery的两个方法: next() : 获得匹配元素集合中每个元素紧邻的下一个同胞元素. after() :在被选元 ...
- noip2017考前整理(未完)
快考试了,把我以前写过的题回顾一下.Noip2007 树网的核:floyd,推出性质,暴力.Noip2008 笨小猴:模拟Noip2008 火柴棒等式:枚举Noip2008 传纸条:棋盘dpNoip2 ...
- RabbitMQ 相关概念
RabbitMQ 整体上是一个生产者与消费者模型,主要负责接收.存储和转发消息.可以把消息传递的过程想象成:当你讲一个包裹送到邮局,邮局会暂存并最终将邮件通过邮递员送到收件人的手上,RabbitMQ ...
- Eureka的原理
http://blog.csdn.net/awschina/article/details/17639191 关于AWS的区域和可用区概念解释: Eureka的原理:Region与Zone. 因为在编 ...