一、前言
先看个例子:
cpp
QVector<int> a, b;
a.resize(100);
QVector<int>::iterator i = a.begin();
b = a;
a[0] = 5;
qDebug() << a[0] << b[0] << *i;
// b.clear(); // Now the iterator i is completely invalid.
*i = 12;
qDebug() << a[0] << b[0] << *i;
输出结果是:
5 0 0
5 12 12
是不是有些出人意料?为何会出现这种现象,原因是QVector 是隐式共享(copy-on-write)的容器。以上代码中,b = a; 并不会马上复制一份独立的数据。它只是让 b 和 a 共享同一块内部数据缓冲区。此时:a 和 b 共享同一个数据内存,i 仍然指向那块内存。QVector<int>::iterator 本质上指向内部数组,当修改 a 或 b 时,若发生写时复制(detach),迭代器可能失效。
修改迭代器 i 就是用了隐式共享容器的 "裸指针式" 修改方式。
-
QVector<int>::iterator i = a.begin();iterator在QVector中本质上就是一个指向内部数组的指针。 -
b = a;a和b共享同一块内存,引用计数加 1。 -
*i = 5;通过这个迭代器直接写入内部数组,这种写法绕过了
QVector的"写时复制"检测机制。
因此结果是:
- 并不是通过
a的成员函数修改数据 QVector无法触发detach()- 所以修改会作用在"共享内存"上,而且行为是不安全、不可预测的
正确理解
QVector的 COW 机制只能保证通过容器的写操作(operator[],replace,append, 等)修改时自动 detach。- 通过
iterator/data()直接修改内部数组,已经脱离了容器管理。 - 这时你得到的是"未定义行为"结果,可能只见到
b改变,或者其它奇怪表现。
正确做法
如果想修改 a,应该这样做:a0 = 5; // 这会触发 detach
如果你要在 shared 后修改其中一个对象,先确保它独立:
a.detach();
a0 = 5;
结论
*i = 5 在这里不是"安全修改 a 的方式",而是绕过了 QVector 的 copy-on-write 机制,所以表现会很奇怪。
当然了,如果用C++的vector就不会出现这种现象,如下所示:
cpp
vector<int> vec1, vec2;
vec1.resize(100);
vector<int>::iterator it = vec1.begin();
vec2 = vec1;
vec1[0] = 5;
vec2.clear();
qDebug() << vec1[0] << vec2[0] << *it;
*it = 12;
qDebug() << vec1[0] << vec2[0] << *it;
输出结果:
5 0 5
12 0 12
二、隐式共享的概念
Qt 隐式共享也称为"写时复制(Copy-On-Write)",它是 Qt 设计中一个重要的性能优化机制。
核心概念
- 多个对象可以共享同一块内部数据
- 只要不修改数据,就不会发生复制
- 一旦需要写操作,才会复制出独立的数据
- 这样既节省内存,又提高拷贝性能
典型行为
cpp
QString a = "hello";
QString b = a; // a 和 b 共享同一份数据
b[0] = 'H'; // b 发生写操作 => 自动 detach,复制独立数据
qDebug() << a << b;
输出结果:"hello" "Hello"
这里 a 和 b 初始共享,只有当 b 修改时,才会进行真正的数据复制。
内部机制
隐式共享通常由这些部分构成:
- 一个共享数据结构,包含真实数据和引用计数
- 一个对象持有指向共享数据的指针
- 引用计数记录当前有多少对象共享这份数据
- 读取操作不影响引用计数
- 写操作先检查引用计数,必要时复制数据
这相当于:
- 共享阶段:0 复制
- 写入阶段:才复制
detach() 和 isDetached()
detach() 是隐式共享的关键:
- 如果引用计数为 1,说明当前对象已独占数据,不做任何事
- 如果引用计数 > 1,则复制数据、减小原数据引用计数、当前对象转向新数据
isDetached() 可以检查当前对象是否已经独占数据。
哪些 Qt 类型支持隐式共享
常见支持隐式共享的 Qt 类型:
- QString
QByteArrayQListQVectorQMapQHashQImageQPixmapQFontQVariantQJsonDocument
这些类型的赋值、拷贝都很轻量。
为什么这么设计
优点:
- 拷贝开销小:赋值只是拷贝指针、增加引用计数
- 读取高效:只要不修改,就避免了大量复制
- 内存利用率高:多个对象共享同一份数据
适合场景:
- 读取多,写入少
- 频繁传递值类型对象
什么时候会复制
通常在以下写操作发生时:
operator[]写入append(),insert(),replace()fill(),remove()QImage::bits()、QByteArray::data()等非 const 获取底层可写指针时
换句话说,只要会修改内部数据,Qt 会自动触发复制。
需要注意的坑
-
直接用非 const 迭代器或
data()修改- 可能绕开自动 detach
- 导致共享数据被同时修改
-
对
const访问不会 detachconst QString &s = a;只是读取,不会复制
-
线程安全
- 隐式共享对象本身是可复制的
- 共享数据的读取是线程安全的
- 但写操作不是线程安全的,不能同时在多个线程修改同一个实例
-
QVector/QList等在 Qt 5/Qt 6 中实现细节不同- 只是语义相同,不用关心底层实现
- 但仍建议不要依赖内部指针行为
什么时候手动调用 detach()
通常不需要,但可以用于优化:
- 你知道马上要修改对象
- 希望提前复制,避免后续多次写时重复 detach
例如:
a.detach();
a0 = 1;
总结
- Qt 隐式共享是"共享只读、修改时复制"的机制
- 它让 QString,
QVector等类型赋值非常高效 - 写操作会自动触发
detach() - 不要通过直接修改底层指针绕过机制
- 线程写入时要特别小心
隐式共享的本质是"让对象的拷贝变成轻量级共享,再在需要写时才付出复制成本"。
三、Qt 中的 detach() 是什么
detach() 是 Qt 隐式共享容器里的一个操作,用于保证对象拥有"独占的数据副本"。
它的作用
在 Qt 里,很多类都是隐式共享的,比如:
- QString
QVectorQImageQByteArrayQMapQHash
当你做赋值 b = a; 时,这些对象通常不会立即复制数据,而是共享同一块内部内存,并且内部维护一个引用计数。
detach() 的作用就是:
- 检查这块共享数据是否被多个对象共享
- 如果是,则复制一份独立的数据
- 让当前对象指向这份独立数据
- 让当前对象"独占"这份数据
- 使引用计数对原共享数据递减
detach() 具体做了哪些工作
通常它会执行以下步骤:
- 读取内部引用计数
- 如果引用计数为 1
- 说明当前对象已经是唯一持有者
- 直接返回,不做额外操作
- 如果引用计数大于 1
- 分配新的内存
- 将原数据复制到新内存
- 将当前对象的内部指针指向新内存
- 原数据的引用计数减 1
- 新数据的引用计数设为 1
什么时候会调用 detach()
一般在"写操作"时自动触发:
a[i] = valuea.append(...)a.replace(...)a.insert(...)a.fill(...)
这些操作会自动调用 detach(),保证修改只影响当前对象,不影响其他共享该数据的对象。
为什么需要 detach()
这是 Qt 的"写时复制"(copy-on-write)机制:
- 读取时不复制,节省内存和性能
- 修改时才复制,保证语义正确
比如:
cpp
QString a = "hello";
QString b = a; // 共享数据
b[0] = 'H'; // 自动 detach,b 得到自己的副本
在这个例子里,a 保持原样,b 先 detach 再修改。
额外提醒
detach()通常是隐式调用的,你很少需要手动调用它- 手动调用
detach()可以提前让对象独立,以避免后续修改造成额外开销 - 直接通过迭代器或
data()修改底层数据,可能绕开detach(),这会导致不安全行为
简单来说,
detach()就是"把共享数据变成独占数据"的过程。